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; } // Documentation text normalized. [Header("UI Effects")] public Material grayScaleMaterial; public Color hurtColor = Color.red; public Color healColor = Color.green; public Color manaColor = Color.blue; [Range(0f, 1f)] public float flashStartAlpha = 1f; [Header("Flash Timing")] public float flashHoldTime = 0.1f; public float flashFadeDuration = 0.2f; private static readonly int SaturationID = Shader.PropertyToID("_Saturation"); [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 }; // Documentation text normalized. [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 }; // Documentation text normalized. private TeamCharacterDataInfo[] currentAllySOs = new TeamCharacterDataInfo[5]; // Documentation text normalized. private EnemyData_SO[] currentEnemySOs = new EnemyData_SO[0]; // Documentation text normalized. public EnemyData_SO[] recognizedEnemySOs = new EnemyData_SO[0]; // previous active flags for detecting external changes private bool[] prevAllyActive = new bool[5]; // Documentation text normalized. [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"; // Documentation text normalized. [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("Victory Animation")] [Tooltip("The object to move when all enemies are defeated")] public GameObject victoryMoveObject; [Tooltip("Target X position (AnchoredPosition X for UI, World X for others)")] public float victoryTargetX; [Tooltip("Movement curve (Time 0->1, Value 0->1 recommended)")] public AnimationCurve victoryMoveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); [Tooltip("Duration of the movement in seconds")] public float victoryMoveDuration = 2f; [Tooltip("Delay before the victory animation starts (seconds)")] public float victoryAnimationDelay = 3f; [Tooltip("The image to fade saturation on victory")] public Image victorySaturationImage; [Tooltip("Duration for the saturation fade (seconds)")] public float victorySaturationDuration = 2f; private Coroutine _victoryMoveCoroutine; private Coroutine _victorySaturationCoroutine; [Header("Ally Statistics (Real-time)")] [Tooltip("Total damage dealt to enemies by each of the 5 ally slots.")] public float[] totalDamageDealt = new float[5]; [Tooltip("Total HP restored by each of the 5 ally slots.")] public float[] totalHealProvided = new float[5]; [Tooltip("Total Mana restored by each of the 5 ally slots.")] public float[] totalManaRestored = new float[5]; [Tooltip("Total damage taken by each of the 5 ally slots.")] public float[] totalDamageTaken = new float[5]; /// /// Record damage dealt by an ally slot. /// public void RecordDamage(int slotIndex, float amount) { if (slotIndex >= 0 && slotIndex < 5) totalDamageDealt[slotIndex] += amount; } /// /// Record damage taken by an ally slot. /// public void RecordDamageTaken(int slotIndex, float amount) { if (slotIndex >= 0 && slotIndex < 5) totalDamageTaken[slotIndex] += amount; } /// /// Record healing provided by an ally slot. /// public void RecordHeal(int slotIndex, float amount) { if (slotIndex >= 0 && slotIndex < 5) totalHealProvided[slotIndex] += amount; } /// /// Record mana restored by an ally slot. /// public void RecordMana(int slotIndex, float amount) { if (slotIndex >= 0 && slotIndex < 5) totalManaRestored[slotIndex] += amount; } // Documentation text normalized. [Header("Editor folder load (Editor only)")] [Tooltip("Deprecated: kept for compatibility. Use editorSOFolderPath or runtimeResourcesFolderPath instead.")] public string selectedProjectFolderPath = string.Empty; /// /// Documentation text normalized. /// Documentation text normalized. /// 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 is outside the project Assets folder. Use a project path like Assets/Resources/so/ally or an absolute disk path."); } } // 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; // Documentation text normalized. 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; } // Documentation text normalized. 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; } } } // Documentation text normalized. 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; // Documentation text normalized. 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})"); // Documentation text normalized. 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; // Documentation text normalized. 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; } // Documentation text normalized. 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; } } // Documentation text normalized. 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) { var cloned = Instantiate(so); list.Add(cloned); } } } 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; } // Documentation text normalized. 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}"); } /// /// Documentation text normalized. /// public TeamCharacterDataInfo[] GetCurrentAllySOs() { return (TeamCharacterDataInfo[])currentAllySOs.Clone(); } [Header("Inspector")] [Tooltip("Documentation text normalized.")] public TextMeshProUGUI songNametitle; public TextMeshProUGUI difficultyName; public TextMeshProUGUI difficultyID; public TextMeshProUGUI constructionName; [Tooltip("Documentation text normalized.")] public Text comboCounter; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI enemyCounter; [SerializeField] private int enemyCounterMax; // Documentation text normalized. [SerializeField] private int enemyCurrentCount; // Documentation text normalized. [Tooltip("Documentation text normalized.")] public TextMeshProUGUI currentTotalScore; [Header("Inspector")] public TextMeshProUGUI allSum_pmScore; [Header("Inspector")] 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; // Documentation text normalized. private Coroutine totalFadeHealthCoroutine; // Documentation text normalized. [Header("Inspector")] public ComboJudgeType comboJudgeType = ComboJudgeType.Perfect; private int combo = 0; public enum ComboJudgeType { Perfect, Great, Good, // Miss } [Header("Inspector")] [Tooltip("Documentation text normalized.")] public GameObject objectFather_ally01; [Tooltip("Documentation text normalized.")] public bool isAlly01_active = true; [Tooltip("Documentation text normalized.")] public Image teammate01_characterImage; [Tooltip("Documentation text normalized.")] public Image teammate01_healthImage; [Tooltip("Documentation text normalized.")] public Image teammate01_fadehealthImage; [Tooltip("Documentation text normalized.")] public Image teammate01_manaImage; [Tooltip("Documentation text normalized.")] public Image teammate01_fadeManaImage; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate01_nameText; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate01_healthRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate01_current_scoreText; // rate : now score / max score [Tooltip("Documentation text normalized.")] [SerializeField] private int ally01_id; [Tooltip("Documentation text normalized.")] public Image teammate01_hurtRedImage; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate01_currentHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate01_maxHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate01_currentMana; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate01_maxMana; [Header("Inspector")] public TextMeshProUGUI red_pmScore_sum; [Header("Inspector")] public TextMeshProUGUI red_idolScore_sum; [Header("Inspector")] [Tooltip("Documentation text normalized.")] public GameObject objectFather_ally02; [Tooltip("Documentation text normalized.")] public bool isAlly02_active = true; [Tooltip("Documentation text normalized.")] public Image teammate02_characterImage; [Tooltip("Documentation text normalized.")] public Image teammate02_healthImage; [Tooltip("Documentation text normalized.")] public Image teammate02_fadehealthImage; [Tooltip("Documentation text normalized.")] public Image teammate02_manaImage; [Tooltip("Documentation text normalized.")] public Image teammate02_fadeManaImage; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate02_nameText; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate02_healthRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate02_current_scoreText; // rate : now score / max score [Tooltip("Documentation text normalized.")] [SerializeField] private int ally02_id; [Tooltip("Documentation text normalized.")] public Image teammate02_hurtRedImage; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate02_currentHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate02_maxHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate02_currentMana; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate02_maxMana; [Header("pmScore")] public TextMeshProUGUI green_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI green_idolScore_sum; [Header("teammate03")] [Tooltip("Documentation text normalized.")] public GameObject objectFather_ally03; [Tooltip("Documentation text normalized.")] public bool isAlly03_active = true; [Tooltip("Documentation text normalized.")] public Image teammate03_characterImage; [Tooltip("Documentation text normalized.")] public Image teammate03_healthImage; [Tooltip("Documentation text normalized.")] public Image teammate03_fadehealthImage; [Tooltip("Documentation text normalized.")] public Image teammate03_manaImage; [Tooltip("Documentation text normalized.")] public Image teammate03_fadeManaImage; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate03_nameText; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate03_healthRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate03_current_scoreText; // rate : now score / max score [Tooltip("Documentation text normalized.")] [SerializeField] private int ally03_id; [Tooltip("Documentation text normalized.")] public Image teammate03_hurtRedImage; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate03_currentHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate03_maxHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate03_currentMana; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate03_maxMana; [Header("pmScore")] public TextMeshProUGUI yellow_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI yellow_idolScore_sum; [Header("teammate04")] [Tooltip("Documentation text normalized.")] public GameObject objectFather_ally04; [Tooltip("Documentation text normalized.")] public bool isAlly04_active = true; [Tooltip("Documentation text normalized.")] public Image teammate04_characterImage; [Tooltip("Documentation text normalized.")] public Image teammate04_healthImage; [Tooltip("Documentation text normalized.")] public Image teammate04_fadehealthImage; [Tooltip("Documentation text normalized.")] public Image teammate04_manaImage; [Tooltip("Documentation text normalized.")] public Image teammate04_fadeManaImage; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate04_nameText; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate04_healthRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate04_current_scoreText; // rate : now score / max score [Tooltip("Documentation text normalized.")] [SerializeField] private int ally04_id; [Tooltip("Documentation text normalized.")] public Image teammate04_hurtRedImage; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate04_currentHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate04_maxHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate04_currentMana; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate04_maxMana; [Header("pmScore")] public TextMeshProUGUI purple_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI purple_idolScore_sum; [Header("teammate05")] [Tooltip("Documentation text normalized.")] public GameObject objectFather_ally05; [Tooltip("Documentation text normalized.")] public bool isAlly05_active = true; [Tooltip("Documentation text normalized.")] public Image teammate05_characterImage; [Tooltip("Documentation text normalized.")] public Image teammate05_healthImage; [Tooltip("Documentation text normalized.")] public Image teammate05_fadehealthImage; [Tooltip("Documentation text normalized.")] public Image teammate05_manaImage; [Tooltip("Documentation text normalized.")] public Image teammate05_fadeManaImage; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate05_nameText; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate05_healthRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI teammate05_current_scoreText; // rate : now score / max score [Tooltip("Documentation text normalized.")] [SerializeField] private int ally05_id; [Tooltip("Documentation text normalized.")] public Image teammate05_hurtRedImage; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate05_currentHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate05_maxHP; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate05_currentMana; [Tooltip("Documentation text normalized.")] [SerializeField] private int teammate05_maxMana; [Header("pmScore")] public TextMeshProUGUI blue_pmScore_sum; [Header("idolScore")] public TextMeshProUGUI blue_idolScore_sum; [Header("currentEnemy")] [Tooltip("Documentation text normalized.")] public GameObject objectFather_enemy; [Tooltip("Documentation text normalized.")] public bool isEnemy_active = true; [Tooltip("Documentation text normalized.")] public Image currentEnemy_characterImage; [Tooltip("Documentation text normalized.")] public Image currentEnemy_healthImage; [Tooltip("Documentation text normalized.")] public Image currentEnemy_fadehealthImage; [Tooltip("Documentation text normalized.")] public Image currentEnemy_manaImage; [Tooltip("Documentation text normalized.")] public Image currentEnemy_fademanaImage; [Tooltip("Documentation text normalized.")] public Image allEnemy_totalHealthImage; [Tooltip("Documentation text normalized.")] public Image allEnemy_totalFadehealthImage; [Tooltip("Documentation text normalized.")] public Text currentEnemy_nameText; [Tooltip("Documentation text normalized.")] public Text currentEnemy_typeText; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI currentEnemy_healthRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI totalEnemy_healthRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI currentEnemy_manaRate; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI currentEnemy_current_scoreText; // rate : now score / max score [Tooltip("Documentation text normalized.")] [SerializeField] private int enemy_id; [Tooltip("Documentation text normalized.")] public Image currentEnemy_hurtRedImage; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI enemyList_rateText; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { // Saturation material assignment moved to PlayVictoryAnimation as requested /* // Initialize saturation image if assigned if (victorySaturationImage != null && grayScaleMaterial != null) { // Assign the grayscale material instance to the image victorySaturationImage.material = new Material(grayScaleMaterial); // Use _Saturation property ID victorySaturationImage.material.SetFloat(SaturationID, 1f); } */ // 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 private float _lastEnemyCheckTime = 0f; private const float ENEMY_CHECK_INTERVAL = 0.1f; // 100ms interval for UI sync void Update() { // detect external changes to isAllyX_active and toggle objects accordingly if (prevAllyActive[0] != isAlly01_active) { ToggleAllyObject(0, isAlly01_active); prevAllyActive[0] = isAlly01_active; } if (prevAllyActive[1] != isAlly02_active) { ToggleAllyObject(1, isAlly02_active); prevAllyActive[1] = isAlly02_active; } if (prevAllyActive[2] != isAlly03_active) { ToggleAllyObject(2, isAlly03_active); prevAllyActive[2] = isAlly03_active; } if (prevAllyActive[3] != isAlly04_active) { ToggleAllyObject(3, isAlly04_active); prevAllyActive[3] = isAlly04_active; } if (prevAllyActive[4] != isAlly05_active) { ToggleAllyObject(4, isAlly05_active); prevAllyActive[4] = isAlly05_active; } // Sync enemy UI with EnemyCombatant values - optimized to not check every single frame if possible if (enemyCombatantInstance != null && Time.time >= _lastEnemyCheckTime + ENEMY_CHECK_INTERVAL) { _lastEnemyCheckTime = Time.time; int hp = enemyCombatantInstance.currentHP; int mana = enemyCombatantInstance.currentMana; if (prevEnemyHP != hp) { UpdateEnemyHealthVisuals(prevEnemyHP, hp, true); // Documentation text normalized. 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); } /// /// Documentation text normalized. /// public void OnJudgeResult(string result) { if (IsCombo(result)) { combo++; } else { combo = 0; } if (comboCounter != null) { _sb.Clear(); _sb.Append(combo); comboCounter.text = _sb.ToString(); } // --- Statistics: Update achievement tracking for combo --- if (InGamePerformanceManager.Instance != null) { InGamePerformanceManager.Instance.UpdateCombo(combo); } else { // Only log once to avoid spamming every frame, or only on combo milestones if (combo > 0 && combo % 10 == 0) { Debug.LogWarning("[teamUIController] InGamePerformanceManager.Instance is null! Cannot update achievements."); } } } /// /// Documentation text normalized. /// 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; } } } } /// /// Documentation text normalized. /// 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(); } /// /// Documentation text normalized. /// 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 (currentEnemy_typeText != null) currentEnemy_typeText.text = "(普通敌人)"; UpdateEnemyListText(); // Documentation text normalized. 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 (currentEnemy_typeText != null) currentEnemy_typeText.text = "(普通敌人)"; UpdateEnemyListText(); // Update total health bar to 0 UpdateAllEnemyTotalHealthUIImmediate(); // Trigger victory animation PlayVictoryAnimation(); 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) string enemyName = so.enemyName ?? so.name; if (currentEnemy_nameText != null) currentEnemy_nameText.text = enemyName; 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; } // Push spawn message to feed SkillTriggerFeedUI.PushEnemySpawn(enemyName); // 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 = "(Boss)"; 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) { _sb.Clear(); _sb.Append(Mathf.Clamp(enemyCurrentCount + 1, 1, 999)).Append("/").Append(enemyCounterMax); enemyCounter.text = _sb.ToString(); } // 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; // Use actual recognized enemy count int total = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0; int displayIndex = 0; if (total > 0) { // enemyCurrentCount is 0-based index // ensure display is 1 to total displayIndex = Mathf.Clamp(enemyCurrentCount + 1, 1, total); } _sb.Clear(); _sb.Append(displayIndex).Append("/").Append(total); enemyList_rateText.text = _sb.ToString(); } private void PlayVictoryAnimation() { // 1. Victory Movement (Delayed) if (victoryMoveObject != null) { if (_victoryMoveCoroutine != null) StopCoroutine(_victoryMoveCoroutine); _victoryMoveCoroutine = StartCoroutine(VictoryMoveRoutine()); } // 2. Victory Saturation Set (Immediate) if (victorySaturationImage != null) { if (_victorySaturationCoroutine != null) StopCoroutine(_victorySaturationCoroutine); // Assign material only when victory occurs if (grayScaleMaterial != null) { victorySaturationImage.material = new Material(grayScaleMaterial); } // Immediately set saturation to 0 (Grayscale) victorySaturationImage.material.SetFloat(SaturationID, 0f); } } private IEnumerator VictoryMoveRoutine() { if (victoryMoveObject == null) yield break; // Wait for the specified delay before starting movement if (victoryAnimationDelay > 0f) yield return new WaitForSeconds(victoryAnimationDelay); float timer = 0f; float duration = victoryMoveDuration > 0 ? victoryMoveDuration : 2f; RectTransform rt = victoryMoveObject.GetComponent(); Transform tf = victoryMoveObject.transform; float startX = 0f; bool isUI = (rt != null); if (isUI) startX = rt.anchoredPosition.x; else startX = tf.position.x; // If curve is not set, default to EaseInOut if (victoryMoveCurve == null || victoryMoveCurve.length == 0) victoryMoveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1); while (timer < duration) { timer += Time.deltaTime; float progress = Mathf.Clamp01(timer / duration); float curveValue = victoryMoveCurve.Evaluate(progress); float newX = Mathf.LerpUnclamped(startX, victoryTargetX, curveValue); if (isUI) { Vector2 pos = rt.anchoredPosition; pos.x = newX; rt.anchoredPosition = pos; } else { Vector3 pos = tf.position; pos.x = newX; tf.position = pos; } yield return null; } // Final set if (isUI) { Vector2 pos = rt.anchoredPosition; pos.x = victoryTargetX; rt.anchoredPosition = pos; } else { Vector3 pos = tf.position; pos.x = victoryTargetX; tf.position = pos; } } // VictorySaturationRoutine removed as requested (immediate set) private void OnEnemyDiedHandler(EnemyCombatant e) { // Push death message to feed before advancing string eName = "Unknown Enemy"; if (enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length) { var so = recognizedEnemySOs[enemyCurrentCount]; if (so != null) eName = so.enemyName ?? so.name; } SkillTriggerFeedUI.PushEnemyDeath(eName); // notify enemy ball UI if (BeatmapManager.Instance != null && BeatmapManager.Instance.enemyBallLoader != null) { BeatmapManager.Instance.enemyBallLoader.OnEnemyDefeated(); } // advance to next enemy after death enemyCurrentCount++; SpawnNextEnemy(); } private void OnEnemyRevivedHandler(EnemyCombatant e) { // refresh UI UpdateEnemyUIImmediate(); } // Documentation text normalized. // ========================================================= // Documentation text normalized. // ========================================================= /// /// Documentation text normalized. /// public void ApplyCalculatedEnemyHP(int individualMaxHP) { List hpList = new List(); if (recognizedEnemySOs != null) { for (int i = 0; i < recognizedEnemySOs.Length; i++) { hpList.Add(individualMaxHP); } } ApplyCalculatedEnemyHP(hpList); } /// /// Apply individual HP values to enemies. /// public void ApplyCalculatedEnemyHP(List individualHPList) { if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0 && individualHPList != null) { Debug.Log($"[teamUIController] Applying calculated HP list to {recognizedEnemySOs.Length} recognized enemies."); for (int i = 0; i < recognizedEnemySOs.Length; i++) { var so = recognizedEnemySOs[i]; if (so != null) { int newHP = (i < individualHPList.Count) ? individualHPList[i] : 0; so.enemy_maxHP = newHP; } } RecalculateTotalMaxHP(); 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: {currentSO.enemy_maxHP}"); enemyCombatantInstance.InitializeFromSO(currentSO); UpdateEnemyUIImmediate(); } } } } // ========================================================= // Documentation text normalized. // ========================================================= /// /// Documentation text normalized. /// 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}"); // Documentation text normalized. UpdateAllEnemyTotalHealthUIImmediate(); } private static void SetBarScaleY(Image img, float ratio01) { if (img == null) return; var tr = img.transform; if (tr == null) return; var s = tr.localScale; s.y = Mathf.Clamp01(ratio01); tr.localScale = s; } private static float GetBarScaleY(Image img) { if (img == null) return 0f; var tr = img.transform; if (tr == null) return 0f; return tr.localScale.y; } // Documentation text normalized. private void UpdateAllEnemyTotalHealthUIOnHpChange(int newCurrentHP) { if (allEnemy_totalHealthImage == null) return; // Documentation text normalized. int totalCur = 0; if (recognizedEnemySOs != null) { for (int i = enemyCurrentCount; i < recognizedEnemySOs.Length; i++) { var so = recognizedEnemySOs[i]; if (so == null) continue; // Documentation text normalized. if (i == enemyCurrentCount) totalCur += Mathf.Max(0, newCurrentHP); else totalCur += Mathf.Max(0, so.enemy_maxHP); } } float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f; // Documentation text normalized. allEnemy_totalHealthImage.fillAmount = newFill; // SetBarScaleY(allEnemy_totalHealthImage, newFill); // SetBarScaleY(allEnemy_totalHealthImage, newFill); if (totalEnemy_healthRate != null) { _sb.Clear(); _sb.Append(totalCur).Append("/").Append(totalMaxHP); totalEnemy_healthRate.text = _sb.ToString(); } // 2. Animate fade bar (only when health decreases) if (allEnemy_totalFadehealthImage != null) { // float currentFadeFill = GetBarScaleY(allEnemy_totalFadehealthImage); 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; // SetBarScaleY(allEnemy_totalFadehealthImage, newFill); } } else if (currentFadeFill < newFill) { // Health increased (healing): update fade bar immediately if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine); allEnemy_totalFadehealthImage.fillAmount = newFill; // SetBarScaleY(allEnemy_totalFadehealthImage, newFill); } // If currentFadeFill == newFill, do nothing. } } // Documentation text normalized. private void UpdateAllEnemyTotalHealthUIImmediate() { if (allEnemy_totalHealthImage == null) return; int totalCur = 0; if (recognizedEnemySOs != null) { for (int i = enemyCurrentCount; i < recognizedEnemySOs.Length; i++) { var so = recognizedEnemySOs[i]; if (so == null) continue; // Documentation text normalized. if (i == enemyCurrentCount) { // Documentation text normalized. if (enemyCombatantInstance != null) totalCur += Mathf.Max(0, enemyCombatantInstance.currentHP); else totalCur += Mathf.Max(0, so.enemy_maxHP); // Fallback } else { totalCur += Mathf.Max(0, so.enemy_maxHP); } } } float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f; // Documentation text normalized. allEnemy_totalHealthImage.fillAmount = newFill; if (allEnemy_totalFadehealthImage != null) { allEnemy_totalFadehealthImage.fillAmount = newFill; // SetBarScaleY(allEnemy_totalFadehealthImage, newFill); } if (totalEnemy_healthRate != null) { _sb.Clear(); _sb.Append(totalCur).Append("/").Append(totalMaxHP); totalEnemy_healthRate.text = _sb.ToString(); } // Documentation text normalized. 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) { _sb.Clear(); _sb.Append(enemyCombatantInstance.currentHP).Append("/").Append(enemyCombatantInstance.maxHP); currentEnemy_healthRate.text = _sb.ToString(); } // 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) { if (enemyCombatantInstance.maxMana > 0) { _sb.Clear(); _sb.Append(enemyCombatantInstance.currentMana).Append("/").Append(enemyCombatantInstance.maxMana); currentEnemy_manaRate.text = _sb.ToString(); } else { currentEnemy_manaRate.text = "0/0"; } } // Aggregate total health logic (modified to call the helper method for animation control) UpdateAllEnemyTotalHealthUIImmediate(); } private System.Text.StringBuilder _sb = new System.Text.StringBuilder(); 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) { _sb.Clear(); _sb.Append(newHP).Append("/").Append(enemyCombatantInstance.maxHP); currentEnemy_healthRate.text = _sb.ToString(); } 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 (Moved to GfxController.PlayHitFX for synchronized hit effects) /* 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) { if (enemyCombatantInstance.maxMana > 0) { _sb.Clear(); _sb.Append(newMana).Append("/").Append(enemyCombatantInstance.maxMana); currentEnemy_manaRate.text = _sb.ToString(); } else { currentEnemy_manaRate.text = "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; } public void TriggerEnemyHurtFlash() { if (currentEnemy_hurtRedImage == null) return; StopCoroutine("EnemyHurtFlash"); StartCoroutine(EnemyHurtFlash()); } 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.02f); float dur = 0.08f; 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; } public void TriggerAllyHurtFlash(int slotIndex) { Image img = null; switch (slotIndex) { case 0: img = teammate01_hurtRedImage; break; case 1: img = teammate02_hurtRedImage; break; case 2: img = teammate03_hurtRedImage; break; case 3: img = teammate04_hurtRedImage; break; case 4: img = teammate05_hurtRedImage; break; } if (img == null) return; StartCoroutine(AllyHurtFlashCoroutine(img)); } private IEnumerator AllyHurtFlashCoroutine(Image img) { if (img == null) yield break; Color c = img.color; c.a = 1f; img.color = c; yield return new WaitForSeconds(0.02f); float dur = 0.08f; float t = 0f; while (t < dur) { t += Time.deltaTime; c.a = Mathf.Lerp(1f, 0f, t / dur); img.color = c; yield return null; } c.a = 0f; img.color = c; } private IEnumerator TotalFadeHealthCoroutine(float targetFill) { if (allEnemy_totalFadehealthImage == null) yield break; // Delay to create the "fade" lag behind the top bar. yield return new WaitForSeconds(0.5f); if (allEnemy_totalFadehealthImage == null) yield break; // float start = GetBarScaleY(allEnemy_totalFadehealthImage); float start = allEnemy_totalFadehealthImage.fillAmount; float duration = 0.6f; float t = 0f; while (t < duration) { t += Time.deltaTime; float y = Mathf.Lerp(start, targetFill, t / duration); allEnemy_totalFadehealthImage.fillAmount = y; // SetBarScaleY(allEnemy_totalFadehealthImage, y); yield return null; } allEnemy_totalFadehealthImage.fillAmount = targetFill; // SetBarScaleY(allEnemy_totalFadehealthImage, targetFill); } // Documentation text normalized. // Documentation text normalized. // Documentation text normalized. public IEnumerator AllyImageFlash(Image hurtImage, Color targetColor) { if (hurtImage == null) yield break; float startAlpha = flashStartAlpha; Color c = targetColor; c.a = startAlpha; hurtImage.color = c; yield return new WaitForSeconds(flashHoldTime); float dur = flashFadeDuration; float t = 0f; while (t < dur) { t += Time.deltaTime; c.a = Mathf.Lerp(startAlpha, 0f, t / dur); hurtImage.color = c; yield return null; } c.a = 0f; hurtImage.color = c; } // Documentation text normalized. // Documentation text normalized. public IEnumerator AllyDeathGrayscale(Image characterImage) { if (characterImage == null || grayScaleMaterial == null) yield break; // Documentation text normalized. Material instanceMat = new Material(grayScaleMaterial); characterImage.material = instanceMat; float dur = 1.0f; float t = 0f; while (t < dur) { t += Time.deltaTime; float sat = Mathf.Lerp(1f, 0f, t / dur); instanceMat.SetFloat(SaturationID, sat); yield return null; } instanceMat.SetFloat(SaturationID, 0f); } // Documentation text normalized. }