using UnityEngine; using Spine; using Spine.Unity; using SmoothShakeFree; using UnityEngine.UI; using TMPro; using System.Collections.Generic; using System.Collections; using DG.Tweening; #if UNITY_EDITOR using UnityEditor; #endif public class teamUIController : MonoBehaviour { private static readonly bool VerboseLogs = false; private static string NoEnemyLabel => LocalizationService.Get("enemy.none", "(No Enemy)"); private static string EnemyDefeatedLabel => LocalizationService.Get("enemy.all_defeated", "所有敌人已被击败"); private static string EnemyTypeNormalLabel => LocalizationService.Get("enemy.type.normal", "(普通敌人)"); private static string EnemyTypeEliteLabel => LocalizationService.Get("enemy.type.elite", "(精英敌人)"); private static string EnemyTypeBossLabel => LocalizationService.Get("enemy.type.boss", "(Boss)"); private static string EnemyTypeLegendLabel => LocalizationService.Get("enemy.type.legend", "(史诗野怪)"); private static string EnemyTypeGuardianLabel => LocalizationService.Get("enemy.type.guardian", "(远古巨龙)"); public static teamUIController Instance { get; private set; } public bool IsAnyAllyActive() { return isAlly01_active || isAlly02_active || isAlly03_active || isAlly04_active || isAlly05_active; } public bool IsAllySlotActive(int slotIndex) { switch (slotIndex) { case 0: return isAlly01_active; case 1: return isAlly02_active; case 2: return isAlly03_active; case 3: return isAlly04_active; case 4: return isAlly05_active; default: return false; } } // 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]; private int CurrentDifficultyID => BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 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; private Coroutine _spineFlashCoroutine; private Coroutine _spineSpawnFadeCoroutine; private bool _pendingSpineSpawnFadeIn; private const float SPINE_SPAWN_FADE_DURATION = 0.5f; [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; SkillBuilder.Instance?.NotifyAllyAttackDealt(slotIndex); } } /// /// 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 = 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); if (VerboseLogs) Debug.Log($"[teamUIController] slot {i + 1} id=0 -> kept active (empty display)"); 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; if (VerboseLogs) 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; if (VerboseLogs) 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})"); // keep UI slot active even when not found 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); } else { if (VerboseLogs) 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 = RuntimeResourcesCache.LoadAllAllyHeroes(); 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 = GetOrBuildRuntimeAllyDiscovered(resourcesPath); 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 = 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; } // keep associated object active ToggleAllyObject(i, true); if (VerboseLogs) Debug.Log($"[teamUIController] (Runtime) slot {i+1} id=0 -> kept active (empty display)"); 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 = 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); continue; } else { // try to get a readable source for the matched object if (string.IsNullOrEmpty(runtimeSourceInfo)) { runtimeSourceInfo = found.name + " (mapped)"; } if (VerboseLogs) 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 { AllyHero_SO matched = GetCachedRuntimeAllyHero(id); 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 } private List GetOrBuildRuntimeAllyDiscovered(string resourcesPath) { resourcesPath = (resourcesPath ?? string.Empty).Trim('/'); if (_runtimeAllyDiscovered != null && _runtimeAllyResourcesPath == resourcesPath) return _runtimeAllyDiscovered; var runtimeDiscovered = new List(); if (!string.IsNullOrEmpty(resourcesPath)) { 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); Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll returned 0 in '{resourcesPath}', fell back to loading all ScriptableObjects ({arrAll.Length})"); } } if (VerboseLogs) 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) runtimeDiscovered.AddRange(arrAll); } } else { var arr = Resources.LoadAll(""); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr); if (VerboseLogs) Debug.Log($"[teamUIController] (Runtime) Loaded {arr?.Length ?? 0} objects from Resources (project-wide)"); } _runtimeAllyResourcesPath = resourcesPath; _runtimeAllyDiscovered = runtimeDiscovered; _cachedRuntimeAllyHeroes = null; _runtimeAllyHeroById = null; return runtimeDiscovered; } private AllyHero_SO GetCachedRuntimeAllyHero(int id) { if (id <= 0) return null; if (_runtimeAllyHeroById != null && _runtimeAllyHeroById.TryGetValue(id, out var cached)) return cached; if (_cachedRuntimeAllyHeroes == null) { _cachedRuntimeAllyHeroes = RuntimeResourcesCache.LoadAllAllyHeroes() ?? System.Array.Empty(); _runtimeAllyHeroById = new Dictionary(); foreach (var hero in _cachedRuntimeAllyHeroes) { if (hero == null) continue; _runtimeAllyHeroById[hero.ally_heroID] = hero; } } return _runtimeAllyHeroById.TryGetValue(id, out var heroFound) ? heroFound : null; } // 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() { if (VerboseLogs) 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 = null; if (_runtimeEnemyDiscovered != null && _runtimeEnemyResourcesPath == resourcesPath) { runtimeDiscovered = _runtimeEnemyDiscovered; } else { runtimeDiscovered = new List(); if (!string.IsNullOrEmpty(resourcesPath)) { resourcesPath = resourcesPath.Trim('/'); try { var arr = Resources.LoadAll(resourcesPath); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr); if (runtimeDiscovered.Count == 0) { var arrAll = Resources.LoadAll(""); if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); } } catch { var arrAll = Resources.LoadAll(""); if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); } } else { var arr = Resources.LoadAll(""); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr); } _runtimeEnemyResourcesPath = resourcesPath; _runtimeEnemyDiscovered = runtimeDiscovered; } 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(); } } // 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)"; if (VerboseLogs) 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. private bool _allEnemiesDefeatedSkillTriggered; [Tooltip("Documentation text normalized.")] public TextMeshProUGUI currentTotalScore; [Header("Inspector")] public TextMeshProUGUI allSum_pmScore; [Header("Inspector")] public TextMeshProUGUI allSum_idolScore; [Header("Inspector")] public Text recentPlusAmountText; [Header("Inspector")] public Image currentScoreRankImage; [Header("Inspector")] public RankConfig currentScoreRankConfig; // 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; private SkeletonDataAsset _currentEnemySkeletonDataAsset; private SkeletonGraphic _enemySkeletonGraphic; private SkeletonAnimation _enemySkeletonAnimation; private float[] _spineBoundsTemp; private SkeletonClipping _spineBoundsClipping; private Coroutine _enemySpineDeathCoroutine; private const string EnemyIdleAnimationName = "idle"; private const string EnemyDieAnimationName = "die"; private string _runtimeEnemyResourcesPath; private List _runtimeEnemyDiscovered; private string _runtimeAllyResourcesPath; private List _runtimeAllyDiscovered; private AllyHero_SO[] _cachedRuntimeAllyHeroes; private Dictionary _runtimeAllyHeroById; // 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 int CurrentCombo => combo; private Tween recentPlusAmountFadeTween; 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("Shown on currentEnemy_characterImage after the last enemy is defeated. If empty, falls back to the last enemy SO sprite.")] public Sprite allEnemiesDefeatedSprite; [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("Hidden after all enemies are defeated.")] public GameObject currentEnemy_bountyObject; [Tooltip("Displays the current enemy bounty score.")] public Text currentEnemy_bountyText; [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; [SerializeField] private GameObject _spineSystemObject; [SerializeField] private GameObject _imgSystemObject; [SerializeField] private RectTransform spine_to_put; [SerializeField] private Image spine_to_put_referenceImage; [SerializeField] private Color spineHurtFlashColor = new Color(1f, 0.2f, 0.2f, 1f); [SerializeField] private float spineUniformHeight = 0f; private Coroutine enemyBountyTextCoroutine; private bool enemyBountyTransitionActive = false; private int currentDisplayedEnemyBounty = 0; public float SpineUniformHeight { get => spineUniformHeight; set => spineUniformHeight = value; } // 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(); } if (recentPlusAmountText != null) { recentPlusAmountText.text = "+0"; Color color = recentPlusAmountText.color; color.a = 0f; recentPlusAmountText.color = color; } if (ScoreManager.Instance != null) { ScoreManager.Instance.RefreshAllScoreUi(); } } // 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); EnsureSpineSystemObject(); EnsureImgSystemObject(); } /// /// Documentation text normalized. /// public void OnJudgeResult(string result) { if (IsCombo(result)) { combo++; ResetRecentPlusAmountVisual(); } else { combo = 0; FadeOutRecentPlusAmount(); } 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."); } } } public void ResetRecentPlusAmountVisual() { if (recentPlusAmountText == null) { return; } if (recentPlusAmountFadeTween != null) { recentPlusAmountFadeTween.Kill(); recentPlusAmountFadeTween = null; } Color color = recentPlusAmountText.color; color.a = 1f; recentPlusAmountText.color = color; } public void FadeOutRecentPlusAmount() { if (recentPlusAmountText == null) { return; } if (recentPlusAmountFadeTween != null) { recentPlusAmountFadeTween.Kill(); } recentPlusAmountFadeTween = recentPlusAmountText .DOFade(0f, 0.25f) .SetUpdate(true) .OnComplete(() => recentPlusAmountFadeTween = null); } /// /// 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 = SceneObjectLookupCache.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(); bool checkPm = lowHint.Contains("pm"); foreach (var t in all) { if (t == null || t.gameObject == null) continue; var lowerName = t.gameObject.name.ToLower(); if (lowerName.Contains(lowHint) || (checkPm && lowerName.Contains("pm"))) 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) { if (VerboseLogs) 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) { if (VerboseLogs) 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 = SceneObjectLookupCache.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; } } private void SetAllyObjectBySlot(int slotIndex, GameObject value) { switch (slotIndex) { case 0: ally01_object = value; break; case 1: ally02_object = value; break; case 2: ally03_object = value; break; case 3: ally04_object = value; break; case 4: ally05_object = value; break; } } private void RefreshTeammateCharacterImageBySlotId(int slotIndex) { if (slotIndex < 0 || slotIndex >= allySlotIds.Count) return; int id = allySlotIds[slotIndex]; if (id <= 0) return; AllyHero_SO[] all = RuntimeResourcesCache.LoadAllAllyHeroes(); for (int i = 0; i < all.Length; i++) { AllyHero_SO so = all[i]; if (so == null || so.ally_heroID != id) continue; Sprite sprite = so.ally_heroProfile != null ? so.ally_heroProfile : so.ally_heroImage; if (sprite != null) { SetTeammateCharacterImage(slotIndex, sprite); } return; } } public void SwapAllyWithLowerAdjacent(int slotIndex) { int lowerSlot = slotIndex + 1; if (slotIndex < 0 || lowerSlot > 4) return; GameObject first = GetAllyObjectBySlot(slotIndex); GameObject second = GetAllyObjectBySlot(lowerSlot); int firstId = slotIndex < allySlotIds.Count ? allySlotIds[slotIndex] : 0; int secondId = lowerSlot < allySlotIds.Count ? allySlotIds[lowerSlot] : 0; SetAllyObjectBySlot(slotIndex, second); SetAllyObjectBySlot(lowerSlot, first); if (slotIndex < allySlotIds.Count) allySlotIds[slotIndex] = secondId; if (lowerSlot < allySlotIds.Count) allySlotIds[lowerSlot] = firstId; if (second != null) { AllyCombatant secondAlly = second.GetComponent(); if (secondAlly != null) secondAlly.slotIndex = slotIndex; } if (first != null) { AllyCombatant firstAlly = first.GetComponent(); if (firstAlly != null) firstAlly.slotIndex = lowerSlot; } RefreshTeammateCharacterImageBySlotId(slotIndex); RefreshTeammateCharacterImageBySlotId(lowerSlot); } // ------------------- Enemy spawn & UI sync helpers ------------------- private void InitializeEnemyInstance() { // Try to find an existing enemy object in scene enemyCombatantInstance = SceneObjectLookupCache.FindFirst(); 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 EnsureSpineSystemObject() { if (spine_to_put != null) { _spineSystemObject = spine_to_put.gameObject; } if (_spineSystemObject == null) { Transform searchRoot = objectFather_enemy != null ? objectFather_enemy.transform : transform; var existing = searchRoot.Find("spinesystemobject"); if (existing != null) { _spineSystemObject = existing.gameObject; } } if (_spineSystemObject == null) { _spineSystemObject = new GameObject("spinesystemobject", typeof(RectTransform)); Transform targetParent = objectFather_enemy != null ? objectFather_enemy.transform : (currentEnemy_characterImage != null ? currentEnemy_characterImage.transform.parent : transform); _spineSystemObject.transform.SetParent(targetParent, false); } if (_enemySkeletonGraphic == null) { _enemySkeletonGraphic = _spineSystemObject.GetComponent(); if (_enemySkeletonGraphic == null) _enemySkeletonGraphic = _spineSystemObject.AddComponent(); } if (_enemySkeletonAnimation == null || _enemySkeletonAnimation.gameObject != _spineSystemObject) { _enemySkeletonAnimation = _spineSystemObject.GetComponent(); if (_enemySkeletonAnimation == null) _enemySkeletonAnimation = _spineSystemObject.AddComponent(); } } private void EnsureImgSystemObject() { if (_imgSystemObject == null) { if (currentEnemy_characterImage != null) { _imgSystemObject = currentEnemy_characterImage.gameObject; } else { Transform searchRoot = objectFather_enemy != null ? objectFather_enemy.transform : transform; var existing = searchRoot.Find("imgSys"); if (existing != null) { _imgSystemObject = existing.gameObject; } } } if (_imgSystemObject == null) { _imgSystemObject = new GameObject("imgSys", typeof(RectTransform)); Transform targetParent = objectFather_enemy != null ? objectFather_enemy.transform : transform; _imgSystemObject.transform.SetParent(targetParent, false); } } private void UpdateSpineRectForHeight(EnemyData_SO so) { if (_enemySkeletonGraphic == null) return; if (so == null) return; var rect = _enemySkeletonGraphic.rectTransform; if (rect == null) return; float targetHeight = spineUniformHeight; if (targetHeight <= 0f && spine_to_put_referenceImage != null) targetHeight = spine_to_put_referenceImage.rectTransform.rect.height; else if (targetHeight <= 0f && currentEnemy_characterImage != null) targetHeight = currentEnemy_characterImage.rectTransform.rect.height; if (targetHeight <= 0f) return; var skeleton = _enemySkeletonGraphic.Skeleton; if (skeleton == null) return; if (_spineBoundsClipping == null) _spineBoundsClipping = new SkeletonClipping(); float minX; float minY; float width; float height; skeleton.GetBounds(out minX, out minY, out width, out height, ref _spineBoundsTemp, _spineBoundsClipping); float skeletonHeight = height; float skeletonWidth = width; if (skeletonHeight <= 0f || skeletonWidth <= 0f) return; float targetWidth = skeletonWidth * (targetHeight / skeletonHeight); float ratio = so.ScaleRatio; if (ratio <= 0f) ratio = 1f; rect.anchorMin = new Vector2(0.5f, 0.5f); rect.anchorMax = new Vector2(0.5f, 0.5f); rect.pivot = new Vector2(0.5f, 0.5f); rect.sizeDelta = new Vector2(targetWidth, targetHeight); rect.anchoredPosition = Vector2.zero; if (spine_to_put != null) { float xScale = so.FlipX ? -ratio : ratio; spine_to_put.localScale = new Vector3(xScale, ratio, 1f); spine_to_put.anchoredPosition = new Vector2(so.XOffset, so.YOffset); } else { rect.sizeDelta = new Vector2(targetWidth * ratio, targetHeight * ratio); rect.anchoredPosition = new Vector2(so.XOffset, so.YOffset); rect.localScale = new Vector3(so.FlipX ? -1f : 1f, 1f, 1f); } } private TrackEntry TryPlayEnemySpineAnimation(string animationName, bool loop) { if (_enemySkeletonGraphic == null) return null; var skeleton = _enemySkeletonGraphic.Skeleton; if (skeleton == null) return null; if (skeleton.Data.FindAnimation(animationName) == null) return null; if (_enemySkeletonAnimation == null) return null; var state = _enemySkeletonAnimation.AnimationState; if (state == null) return null; return state.SetAnimation(0, animationName, loop); } private IEnumerator PlayEnemyDieThenAdvance(float delay) { yield return new WaitForSeconds(delay); _enemySpineDeathCoroutine = null; // Check if this was the last enemy and it was using Spine if (enemyCurrentCount == recognizedEnemySOs.Length - 1) { var so = recognizedEnemySOs[enemyCurrentCount]; if (so != null && so.skeletonDataAsset != null) { // Start transition to static image for the last Spine enemy if (gameObject.activeInHierarchy) StartCoroutine(TransitionSpineToStaticImage(so)); yield break; // Do not increment enemyCurrentCount or call SpawnNextEnemy yet } } enemyCurrentCount++; SpawnNextEnemy(); } private IEnumerator TransitionSpineToStaticImage(EnemyData_SO so) { // 1. Ensure Img System Object is ready EnsureImgSystemObject(); if (_imgSystemObject != null) _imgSystemObject.SetActive(true); // Stop any pending hurt flash StopCoroutine("EnemyHurtFlash"); if (_spineFlashCoroutine != null) { StopCoroutine(_spineFlashCoroutine); _spineFlashCoroutine = null; } // 2. Setup the static image if (currentEnemy_hurtRedImage != null) { Color hurtColor = currentEnemy_hurtRedImage.color; hurtColor.a = 0f; currentEnemy_hurtRedImage.color = hurtColor; } if (currentEnemy_characterImage != null) { // Use the configured victory sprite first, otherwise fall back to the defeated enemy sprite. var sprite = allEnemiesDefeatedSprite != null ? allEnemiesDefeatedSprite : (so.enemy_Image != null ? so.enemy_Image : (so.enemy_Profile != null ? so.enemy_Profile : null)); currentEnemy_characterImage.sprite = sprite; // Apply grayscale material if (grayScaleMaterial != null) { currentEnemy_characterImage.material = new Material(grayScaleMaterial); currentEnemy_characterImage.material.SetFloat(SaturationID, 0f); // 0 means grayscale } // Start with alpha 0 for fade in Color c = currentEnemy_characterImage.color; c.a = 0f; currentEnemy_characterImage.color = c; // 3. Fade in over 1 second float timer = 0f; float duration = 1.0f; while (timer < duration) { timer += Time.deltaTime; float alpha = Mathf.Clamp01(timer / duration); c.a = alpha; currentEnemy_characterImage.color = c; yield return null; } // Ensure fully opaque at the end c.a = 1f; currentEnemy_characterImage.color = c; } // 4. Finally advance to the "All Defeated" state if (currentEnemy_hurtRedImage != null) { Color hurtColor = currentEnemy_hurtRedImage.color; hurtColor.a = 0f; currentEnemy_hurtRedImage.color = hurtColor; } enemyCurrentCount++; SpawnNextEnemy(); } private void ApplyEnemyVisuals(EnemyData_SO so) { if (so == null) { _currentEnemySkeletonDataAsset = null; if (_spineSystemObject != null) _spineSystemObject.SetActive(false); if (_imgSystemObject != null) _imgSystemObject.SetActive(false); if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null; return; } if (so.skeletonDataAsset != null) { EnsureSpineSystemObject(); EnsureImgSystemObject(); if (_currentEnemySkeletonDataAsset != so.skeletonDataAsset) { _currentEnemySkeletonDataAsset = so.skeletonDataAsset; if (_enemySkeletonAnimation != null) { _enemySkeletonAnimation.skeletonDataAsset = _currentEnemySkeletonDataAsset; _enemySkeletonAnimation.Initialize(true); } if (_enemySkeletonGraphic != null) { _enemySkeletonGraphic.skeletonDataAsset = _currentEnemySkeletonDataAsset; _enemySkeletonGraphic.Initialize(true); } } if (_enemySkeletonGraphic != null) { UpdateSpineRectForHeight(so); TryPlayEnemySpineAnimation(EnemyIdleAnimationName, true); } if (_spineSystemObject != null) _spineSystemObject.SetActive(true); if (_imgSystemObject != null) _imgSystemObject.SetActive(false); if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null; return; } _currentEnemySkeletonDataAsset = null; if (_spineSystemObject != null) _spineSystemObject.SetActive(false); EnsureImgSystemObject(); if (_imgSystemObject != null) _imgSystemObject.SetActive(true); 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; } } private void SpawnNextEnemy() { if (_enemySpineDeathCoroutine != null) { StopCoroutine(_enemySpineDeathCoroutine); _enemySpineDeathCoroutine = null; } // handle case: no configured/recognized enemies -> show empty state but keep UI visible if (recognizedEnemySOs == null || recognizedEnemySOs.Length == 0) { _currentEnemySkeletonDataAsset = null; if (_spineSystemObject != null) _spineSystemObject.SetActive(false); if (_imgSystemObject != null) _imgSystemObject.SetActive(false); if (objectFather_enemy != null) objectFather_enemy.SetActive(true); if (currentEnemy_nameText != null) currentEnemy_nameText.text = NoEnemyLabel; 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 = EnemyTypeNormalLabel; HideEnemyBountyUiImmediate(); UpdateEnemyListText(); // Documentation text normalized. UpdateAllEnemyTotalHealthUIImmediate(); return; } if (enemyCurrentCount < 0) enemyCurrentCount = 0; if (enemyCurrentCount == 0) _allEnemiesDefeatedSkillTriggered = false; if (enemyCurrentCount >= recognizedEnemySOs.Length) { // All enemies processed: show empty/finished state but keep UI visible _currentEnemySkeletonDataAsset = null; if (_spineSystemObject != null) _spineSystemObject.SetActive(false); // Fix: Keep _imgSystemObject active if we just transitioned to a static image if (_imgSystemObject != null && !(_imgSystemObject.activeSelf && currentEnemy_characterImage != null && currentEnemy_characterImage.sprite != null)) { _imgSystemObject.SetActive(false); } else if (_imgSystemObject != null) { // Ensure it stays active _imgSystemObject.SetActive(true); } if (objectFather_enemy != null) objectFather_enemy.SetActive(true); if (currentEnemy_hurtRedImage != null) { Color hurtColor = currentEnemy_hurtRedImage.color; hurtColor.a = 0f; currentEnemy_hurtRedImage.color = hurtColor; } if (currentEnemy_nameText != null) currentEnemy_nameText.text = EnemyDefeatedLabel; // 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 = EnemyTypeNormalLabel; if (!enemyBountyTransitionActive) HideEnemyBountyUiImmediate(); UpdateEnemyListText(); // Update total health bar to 0 UpdateAllEnemyTotalHealthUIImmediate(); // Trigger victory animation PlayVictoryAnimation(); // Fire "all enemies defeated" skill trigger exactly once for this run. if (!_allEnemiesDefeatedSkillTriggered && Application.isPlaying) { _allEnemiesDefeatedSkillTriggered = true; if (SkillBuilder.Instance != null) { SkillBuilder.Instance.TriggerOnAllEnemiesDefeated(); if (VerboseLogs) Debug.Log("[teamUIController] All enemies defeated -> TriggerOnAllEnemiesDefeated fired."); } else { var sb = SceneObjectLookupCache.FindAny(); if (sb != null) { sb.TriggerOnAllEnemiesDefeated(); Debug.LogWarning("[teamUIController] SkillBuilder.Instance was null when firing AllEnemiesDefeated; used FindObjectOfType fallback"); } } } 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(); int difficultyID = BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0; enemyCombatantInstance.InitializeFromSO(so, difficultyID); // set UI visuals (name / sprites) string enemyName = so.enemyName ?? so.name; if (currentEnemy_nameText != null) currentEnemy_nameText.text = enemyName; ApplyEnemyVisuals(so); TryStartEnemySpineSpawnFade(so); // 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 = EnemyTypeEliteLabel; break; case EnemyData_SO.EnemyType.Boss: currentEnemy_typeText.text = EnemyTypeBossLabel; break; case EnemyData_SO.EnemyType.Legend: currentEnemy_typeText.text = EnemyTypeLegendLabel; break; case EnemyData_SO.EnemyType.Guardian: currentEnemy_typeText.text = EnemyTypeGuardianLabel; break; default: currentEnemy_typeText.text = EnemyTypeNormalLabel; break; } } // ensure UI parent is visible if (objectFather_enemy != null) objectFather_enemy.SetActive(true); int currentEnemyBounty = CalculateEnemyBountyScore(so); if (currentEnemy_bountyObject != null) currentEnemy_bountyObject.SetActive(true); if (enemyCurrentCount == 0 && currentDisplayedEnemyBounty == 0 && !enemyBountyTransitionActive) { if (enemyBountyTextCoroutine != null) { StopCoroutine(enemyBountyTextCoroutine); enemyBountyTextCoroutine = null; } SetEnemyBountyDisplayImmediate(currentEnemyBounty); } else { AnimateEnemyBountyRise(currentEnemyBounty); } // 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); if (gameObject.activeInHierarchy) _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"; EnemyData_SO so = null; if (enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length) { 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(); } if (so != null && so.skeletonDataAsset != null) { var entry = TryPlayEnemySpineAnimation(EnemyDieAnimationName, false); float delay = 0f; if (entry != null) { delay = entry.AnimationEnd - entry.AnimationStart; if (delay <= 0f && entry.Animation != null) delay = entry.Animation.Duration; } if (delay > 0f) { int nextIndex = enemyCurrentCount + 1; if (nextIndex >= recognizedEnemySOs.Length) { AnimateEnemyBountyToZero(true); } else { AnimateEnemyBountyToZero(false); } if (_enemySpineDeathCoroutine != null) { StopCoroutine(_enemySpineDeathCoroutine); _enemySpineDeathCoroutine = null; } _pendingSpineSpawnFadeIn = true; if (gameObject.activeInHierarchy) _enemySpineDeathCoroutine = StartCoroutine(PlayEnemyDieThenAdvance(delay)); return; } } int nextEnemyIndex = enemyCurrentCount + 1; if (nextEnemyIndex >= recognizedEnemySOs.Length) { AnimateEnemyBountyToZero(true); } else { AnimateEnemyBountyToZero(false); } enemyCurrentCount++; SpawnNextEnemy(); } private void OnEnemyRevivedHandler(EnemyCombatant e) { // refresh UI UpdateEnemyUIImmediate(); } private int CalculateEnemyBountyScore(EnemyData_SO so) { if (so == null) return 0; int difficultyID = BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0; EnemyData_SO.EnemyDifficultyStats stats = so.GetStatsByDifficultyID(difficultyID); int calculatedMaxHp = Mathf.Max(0, stats.enemy_maxHP); float difficultyLevel = ResolveCurrentDifficultyLevel(difficultyID); float totalBounty = calculatedMaxHp * 0.05f * difficultyLevel + so.enemyBountyBonus; return Mathf.Max(0, Mathf.CeilToInt(totalBounty)); } private float ResolveCurrentDifficultyLevel(int difficultyID) { BeatmapManager beatmapManager = BeatmapManager.Instance; if (beatmapManager == null || beatmapManager.assignedSongData == null || beatmapManager.assignedSongData.chartFiles == null) return 1f; List chartFiles = beatmapManager.assignedSongData.chartFiles; for (int i = 0; i < chartFiles.Count; i++) { ChartFileEntry entry = chartFiles[i]; if (entry == null || entry.difficulty != difficultyID) continue; return Mathf.Max(0f, entry.difficultyLEVEL); } return 1f; } private void SetEnemyBountyDisplayImmediate(int value) { currentDisplayedEnemyBounty = Mathf.Max(0, value); if (currentEnemy_bountyText != null) currentEnemy_bountyText.text = currentDisplayedEnemyBounty.ToString(); } private void HideEnemyBountyUiImmediate() { if (enemyBountyTextCoroutine != null) { StopCoroutine(enemyBountyTextCoroutine); enemyBountyTextCoroutine = null; } enemyBountyTransitionActive = false; SetEnemyBountyDisplayImmediate(0); if (currentEnemy_bountyObject != null) currentEnemy_bountyObject.SetActive(false); } private void AnimateEnemyBountyRise(int targetValue) { if (enemyBountyTextCoroutine != null) StopCoroutine(enemyBountyTextCoroutine); if (currentEnemy_bountyObject != null) currentEnemy_bountyObject.SetActive(true); enemyBountyTransitionActive = true; enemyBountyTextCoroutine = StartCoroutine(AnimateEnemyBountyValueRoutine(0, Mathf.Max(0, targetValue), 1f, false)); } private void AnimateEnemyBountyToZero(bool hideAfterZero) { if (enemyBountyTextCoroutine != null) StopCoroutine(enemyBountyTextCoroutine); if (currentEnemy_bountyObject != null) currentEnemy_bountyObject.SetActive(true); enemyBountyTransitionActive = true; enemyBountyTextCoroutine = StartCoroutine(AnimateEnemyBountyValueRoutine(currentDisplayedEnemyBounty, 0, 1f, hideAfterZero)); } private IEnumerator AnimateEnemyBountyValueRoutine(int startValue, int endValue, float duration, bool deactivateAtEnd) { enemyBountyTransitionActive = true; SetEnemyBountyDisplayImmediate(startValue); if (duration <= 0f) { SetEnemyBountyDisplayImmediate(endValue); } else { float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = Mathf.Clamp01(elapsed / duration); int value = Mathf.RoundToInt(Mathf.Lerp(startValue, endValue, t)); SetEnemyBountyDisplayImmediate(value); yield return null; } SetEnemyBountyDisplayImmediate(endValue); } enemyBountyTransitionActive = false; enemyBountyTextCoroutine = null; if (deactivateAtEnd && currentEnemy_bountyObject != null) currentEnemy_bountyObject.SetActive(false); } // 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) { if (VerboseLogs) 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.SetMaxHPByDifficulty(CurrentDifficultyID, newHP); } } RecalculateTotalMaxHP(); if (enemyCombatantInstance != null && enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length) { var currentSO = recognizedEnemySOs[enemyCurrentCount]; if (currentSO != null) { int diffID = BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0; var stats = currentSO.GetStatsByDifficultyID(diffID); if (VerboseLogs) Debug.Log($"[teamUIController] Re-initializing current enemy instance with new HP: {stats.enemy_maxHP}"); enemyCombatantInstance.InitializeFromSO(currentSO, diffID); UpdateEnemyUIImmediate(); // The first enemy can be spawned before BeatmapManager finishes writing // the calculated HP back into the enemy SO. Refresh bounty text here so it // reflects the final computed max HP rather than the pre-calculation value. int refreshedBounty = CalculateEnemyBountyScore(currentSO); if (currentEnemy_bountyObject != null) currentEnemy_bountyObject.SetActive(true); if (!enemyBountyTransitionActive) SetEnemyBountyDisplayImmediate(refreshedBounty); } } } } // ========================================================= // Documentation text normalized. // ========================================================= /// /// Documentation text normalized. /// private void RecalculateTotalMaxHP() { totalMaxHP = 0; if (recognizedEnemySOs != null) { foreach (var so in recognizedEnemySOs) { if (so != null) totalMaxHP += so.GetStatsByDifficultyID(CurrentDifficultyID).enemy_maxHP; } } if (VerboseLogs) 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.GetStatsByDifficultyID(CurrentDifficultyID).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.GetStatsByDifficultyID(CurrentDifficultyID).enemy_maxHP); // Fallback } else { totalCur += Mathf.Max(0, so.GetStatsByDifficultyID(CurrentDifficultyID).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 && gameObject.activeInHierarchy) 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 && gameObject.activeInHierarchy) 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() { // Don't flash if all enemies are defeated or if we are in the middle of a spine death transition if (enemyCurrentCount >= recognizedEnemySOs.Length || _enemySpineDeathCoroutine != null) return; if (_currentEnemySkeletonDataAsset != null && _enemySkeletonGraphic != null) { if (_spineFlashCoroutine != null) StopCoroutine(_spineFlashCoroutine); _enemySkeletonGraphic.color = Color.white; _enemySkeletonGraphic.Skeleton.SetColor(Color.white); _enemySkeletonGraphic.UpdateMesh(true); _spineFlashCoroutine = StartCoroutine(SpineHurtFlash()); return; } 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; } private IEnumerator SpineHurtFlash() { if (_enemySkeletonGraphic == null) yield break; Color original = _enemySkeletonGraphic.color; Color originalSkeleton = _enemySkeletonGraphic.Skeleton.GetColor(); Color target = spineHurtFlashColor; target.a = 1f; _enemySkeletonGraphic.color = target; _enemySkeletonGraphic.Skeleton.SetColor(target); _enemySkeletonGraphic.UpdateMesh(true); yield return new WaitForSeconds(0.02f); float dur = 0.08f; float t = 0f; while (t < dur) { t += Time.deltaTime; Color lerped = Color.Lerp(target, original, t / dur); _enemySkeletonGraphic.color = lerped; _enemySkeletonGraphic.Skeleton.SetColor(lerped); yield return null; } _enemySkeletonGraphic.color = original; _enemySkeletonGraphic.Skeleton.SetColor(originalSkeleton); _enemySkeletonGraphic.UpdateMesh(true); _spineFlashCoroutine = null; } private void TryStartEnemySpineSpawnFade(EnemyData_SO so) { if (_spineSpawnFadeCoroutine != null) { StopCoroutine(_spineSpawnFadeCoroutine); _spineSpawnFadeCoroutine = null; } if (!_pendingSpineSpawnFadeIn) { return; } _pendingSpineSpawnFadeIn = false; if (so == null || so.skeletonDataAsset == null || _enemySkeletonGraphic == null) { return; } _spineSpawnFadeCoroutine = StartCoroutine(EnemySpineSpawnFade(SPINE_SPAWN_FADE_DURATION)); } private IEnumerator EnemySpineSpawnFade(float duration) { if (_enemySkeletonGraphic == null) { _spineSpawnFadeCoroutine = null; yield break; } var skeleton = _enemySkeletonGraphic.Skeleton; if (skeleton == null) { _spineSpawnFadeCoroutine = null; yield break; } Color baseColor = _enemySkeletonGraphic.color; Color baseSkeleton = skeleton.GetColor(); baseColor.a = 1f; baseSkeleton.a = 1f; Color startColor = baseColor; Color startSkeleton = baseSkeleton; startColor.a = 0f; startSkeleton.a = 0f; _enemySkeletonGraphic.color = startColor; skeleton.SetColor(startSkeleton); _enemySkeletonGraphic.UpdateMesh(true); if (duration <= 0f) { _enemySkeletonGraphic.color = baseColor; skeleton.SetColor(baseSkeleton); _enemySkeletonGraphic.UpdateMesh(true); _spineSpawnFadeCoroutine = null; yield break; } float t = 0f; while (t < duration) { t += Time.deltaTime; float alpha = Mathf.Clamp01(t / duration); Color c = baseColor; Color sc = baseSkeleton; c.a = alpha; sc.a = alpha; _enemySkeletonGraphic.color = c; skeleton.SetColor(sc); _enemySkeletonGraphic.UpdateMesh(true); yield return null; } _enemySkeletonGraphic.color = baseColor; skeleton.SetColor(baseSkeleton); _enemySkeletonGraphic.UpdateMesh(true); _spineSpawnFadeCoroutine = null; } public void TriggerEnemySpineShake(Vector3 amplitude, Vector3 frequency, float duration) { if (_currentEnemySkeletonDataAsset == null) return; if (spine_to_put == null) return; SmoothShake ss = spine_to_put.GetComponent(); if (ss == null) ss = spine_to_put.gameObject.AddComponent(); if (ss.positionShake == null) ss.positionShake = new Shaker(); if (ss.rotationShake == null) ss.rotationShake = new Shaker(); Vector3 halfAmplitude = amplitude * 0.5f; ss.positionShake.noiseType = Shaker.NoiseType.SineWave; ss.positionShake.amplitude = halfAmplitude; ss.positionShake.frequency = frequency; ss.rotationShake.amplitude = Vector3.zero; ss.timeSettings.constantShake = false; ss.timeSettings.fadeInDuration = 0.05f; ss.timeSettings.holdDuration = duration * 0.4f; ss.timeSettings.fadeOutDuration = duration * 0.55f; if (ss.timeSettings.fadeInCurve == null || ss.timeSettings.fadeInCurve.length == 0) ss.timeSettings.fadeInCurve = AnimationCurve.Linear(0, 0, 1, 1); if (ss.timeSettings.fadeOutCurve == null || ss.timeSettings.fadeOutCurve.length == 0) ss.timeSettings.fadeOutCurve = AnimationCurve.Linear(0, 1, 1, 0); ss.enabled = false; ss.enabled = true; } 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; if (gameObject.activeInHierarchy) 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. }