Files
bansonic_beta_main/Assets/scripts/gameplay_teamUI/teamUIController.cs
T

1814 lines
78 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
using System.Collections;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class teamUIController : MonoBehaviour
{
public static teamUIController Instance
{
get;
private set;
}
// --- ID бϵ 5 Ѿ ---
[Header("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;
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<int> allySlotIds = new List<int> { 0, 0, 0, 0, 0 };
// --- ˶в ID б˳ ---
[Header("Runtime enemy configuration")]
[Tooltip("IDs for enemies to appear in sequence. All enemies share a single GameObject and will be initialized from these SOs in order.")]
public List<int> enemySlotIds = new List<int>() { 0, 0, 0, 0, 0 };
// --- ĵǰ SO ã˳ ---
private TeamCharacterDataInfo[] currentAllySOs = new TeamCharacterDataInfo[5];
// ĵǰ SO б˳
private EnemyData_SO[] currentEnemySOs = new EnemyData_SO[0];
// ѽġЧĵ SO id==0 δҵ
private EnemyData_SO[] recognizedEnemySOs = new EnemyData_SO[0];
// previous active flags for detecting external changes
private bool[] prevAllyActive = new bool[5];
// ·ֶΣһڱ༭ңд Assets/Resources/so/ally ̾·
// һʱңResources · so/ally
[Header("SO folder paths")]
[Tooltip("Editor-only: project path or absolute disk path to the folder that contains SOs. Example: Assets/Resources/so/ally")]
public string editorSOFolderPath = "Assets/Resources/so/ally";
[Tooltip("Runtime: Resources subfolder path (no 'Resources/' prefix). Example: so/ally")]
public string runtimeResourcesFolderPath = "so/ally";
// SO ·Editor / Runtime
[Tooltip("Editor-only: project path or absolute disk path to the folder that contains enemy SOs. Example: Assets/Resources/so/enemies")]
public string editorEnemySOFolderPath = "Assets/Resources/so/enemies";
[Tooltip("Runtime: Resources subfolder path (no 'Resources/' prefix) for enemy SOs. Example: so/enemies")]
public string runtimeEnemyResourcesFolderPath = "so/enemies";
// Optional per-ally object to toggle together with isAllyX_active. If null falls back to objectFather_allyXX.
[Header("Ally runtime objects (optional)")]
public GameObject ally01_object;
public GameObject ally02_object;
public GameObject ally03_object;
public GameObject ally04_object;
public GameObject ally05_object;
[Header("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];
/// <summary>
/// Record damage dealt by an ally slot.
/// </summary>
public void RecordDamage(int slotIndex, float amount)
{
if (slotIndex >= 0 && slotIndex < 5)
totalDamageDealt[slotIndex] += amount;
}
/// <summary>
/// Record damage taken by an ally slot.
/// </summary>
public void RecordDamageTaken(int slotIndex, float amount)
{
if (slotIndex >= 0 && slotIndex < 5)
totalDamageTaken[slotIndex] += amount;
}
/// <summary>
/// Record healing provided by an ally slot.
/// </summary>
public void RecordHeal(int slotIndex, float amount)
{
if (slotIndex >= 0 && slotIndex < 5)
totalHealProvided[slotIndex] += amount;
}
/// <summary>
/// Record mana restored by an ally slot.
/// </summary>
public void RecordMana(int slotIndex, float amount)
{
if (slotIndex >= 0 && slotIndex < 5)
totalManaRestored[slotIndex] += amount;
}
// ݾֶΣѡ
[Header("Editor folder load (Editor only)")]
[Tooltip("Deprecated: kept for compatibility. Use editorSOFolderPath or runtimeResourcesFolderPath instead.")]
public string selectedProjectFolderPath = string.Empty;
/// <summary>
/// /еã allySlotIds5 intָļӦ TeamCharacterDataInfo ʲ
/// Editor: ʹ editorSOFolderPath / selectedProjectFolderPathRuntime: ʹ runtimeResourcesFolderPath / selectedProjectFolderPath
/// </summary>
public void PopulateAllySOsFromIds()
{
if (allySlotIds == null || allySlotIds.Count < 5)
{
Debug.LogWarning("allySlotIds δȷãԶΪ 5 ռλ ID");
allySlotIds = new List<int> { 0, 0, 0, 0, 0 };
}
#if UNITY_EDITOR
// Choose editor folder path preferentially from editorSOFolderPath, fallback to selectedProjectFolderPath
string folderToUse = !string.IsNullOrEmpty(editorSOFolderPath) ? editorSOFolderPath : selectedProjectFolderPath;
// Ensure folderToUse points inside the project Assets folder when possible
string projectRelative = null;
if (!string.IsNullOrEmpty(folderToUse))
{
string projectPath = folderToUse.Replace("\\", "/");
if (projectPath.StartsWith(Application.dataPath))
{
projectRelative = "Assets" + projectPath.Substring(Application.dataPath.Length);
}
else if (projectPath.StartsWith("Assets/"))
{
projectRelative = projectPath;
}
else
{
Debug.LogWarning("editorSOFolderPath λĿ Assets ļԱвѯд Assets/Resources/so/ally˵ȫбҡ·ҲдΪ̾·");
}
}
// Collect TeamCharacterDataInfo and AllyHero_SO assets under the folder (or project-wide)
List<Object> discovered = new List<Object>();
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<TeamCharacterDataInfo>(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<AllyHero_SO>(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<TeamCharacterDataInfo>(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<AllyHero_SO>(assetPath);
if (so != null) discovered.Add(so);
}
}
TeamCharacterList globalList = null;
if (teamSettingPanel.Instance != null)
globalList = teamSettingPanel.Instance.teamCharacterList;
for (int i = 0; i < 5; i++)
{
int id = allySlotIds[i];
TeamCharacterDataInfo found = null;
// slot ID Ϊ 0 -> رնӦ ally UI
if (id == 0)
{
currentAllySOs[i] = null;
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
ToggleAllyObject(i, false);
Debug.Log($"[teamUIController] slot {i + 1} id=0 -> disabled");
continue;
}
// ֻҪ asset ·ư id ãEditor
for (int j = 0; j < discovered.Count; j++)
{
var obj = discovered[j];
if (obj == null) continue;
string path = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(path) && path.Contains(id.ToString()))
{
// if it's already TeamCharacterDataInfo, use it
if (obj is TeamCharacterDataInfo tc)
{
found = tc;
Debug.Log($"[teamUIController] (Editor) matched asset by path: {path} for id={id} slot={i + 1}");
break;
}
// if it's AllyHero_SO, map to a runtime TeamCharacterDataInfo instance
if (obj is AllyHero_SO ah)
{
var mapped = ScriptableObject.CreateInstance<TeamCharacterDataInfo>();
mapped.CharacterID = ah.ally_heroID;
mapped.CharacterName = ah.ally_heroName;
mapped.CharacterCardSprite = ah.ally_heroImage;
mapped.CharacterTeamSprite = ah.ally_heroProfile;
mapped.name = ah.name;
found = mapped;
Debug.Log($"[teamUIController] (Editor) matched AllyHero_SO by path: {path} for id={id} slot={i + 1}");
break;
}
}
}
// ˵ȫ teamCharacterList
if (found == null && globalList != null && globalList.characters != null)
{
for (int j = 0; j < globalList.characters.Count; j++)
{
var so = globalList.characters[j];
if (so == null) continue;
// globalList Ҳ name id Ϊ׼
if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString()))
{
found = so;
break;
}
}
}
currentAllySOs[i] = found;
if (found == null)
{
Debug.LogWarning($"[teamUIController] δҵ id={id} Ӧ SO (slot {i + 1})");
// disable UI slot when not found
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
ToggleAllyObject(i, false);
}
else
{
Debug.Log($"[teamUIController] (Editor) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID})");
// ȷӦ UI slot
switch (i)
{
case 0: isAlly01_active = true; break;
case 1: isAlly02_active = true; break;
case 2: isAlly03_active = true; break;
case 3: isAlly04_active = true; break;
case 4: isAlly05_active = true; break;
}
ToggleAllyObject(i, true);
DebugCharacterSO(found, i + 1, true);
// Also attempt to set the teammate character image from an underlying AllyHero_SO if available
try
{
// first try to find AllyHero_SO by the same id
var ahList = Resources.LoadAll<AllyHero_SO>("");
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<Object> runtimeDiscovered = new List<Object>();
if (!string.IsNullOrEmpty(resourcesPath))
{
resourcesPath = resourcesPath.Trim('/');
try
{
var arr = Resources.LoadAll(resourcesPath);
if (arr != null && arr.Length > 0)
runtimeDiscovered.AddRange(arr);
// If nothing loaded from the specific folder, fallback to loading all and filter by name later
if (runtimeDiscovered.Count == 0)
{
var arrAll = Resources.LoadAll("");
if (arrAll != null && arrAll.Length > 0) // Fixed: changed arr.Length to arrAll.Length
{
runtimeDiscovered.AddRange(arrAll);
Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll returned 0 in '{resourcesPath}', fell back to loading all ScriptableObjects ({arrAll.Length})");
}
}
Debug.Log($"[teamUIcontroller] (Runtime) Loaded {runtimeDiscovered.Count} objects from Resources/{resourcesPath}");
}
catch (System.Exception ex)
{
Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll failed for '{resourcesPath}': {ex.Message}");
var arrAll = Resources.LoadAll("");
if (arrAll != null && arrAll.Length > 0) // Fixed: changed arr.Length to arrAll.Length
runtimeDiscovered.AddRange(arrAll);
}
}
else
{
var arr = Resources.LoadAll("");
if (arr != null && arr.Length > 0)
runtimeDiscovered.AddRange(arr);
Debug.Log($"[teamUIController] (Runtime) Loaded {arr?.Length ?? 0} objects from Resources (project-wide)");
}
TeamCharacterList globalList = null;
if (teamSettingPanel.Instance != null)
globalList = teamSettingPanel.Instance.teamCharacterList;
for (int i = 0; i < 5; i++)
{
int id = allySlotIds[i];
TeamCharacterDataInfo found = null;
// slot ID Ϊ 0 -> رնӦ ally UI
if (id == 0)
{
currentAllySOs[i] = null;
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
// also toggle associated object
ToggleAllyObject(i, false);
Debug.Log($"[teamUIController] (Runtime) slot {i+1} id=0 -> disabled");
continue;
}
// ֻҪԴ id ãRuntime֧ TeamCharacterDataInfo AllyHero_SO
string runtimeSourceInfo = string.Empty;
for (int j = 0; j < runtimeDiscovered.Count; j++)
{
var obj = runtimeDiscovered[j];
if (obj == null) continue;
// name match first
if (!string.IsNullOrEmpty(obj.name) && obj.name.Contains(id.ToString()))
{
if (obj is TeamCharacterDataInfo tc)
{
found = tc; break;
}
if (obj is AllyHero_SO ah)
{
var mapped = ScriptableObject.CreateInstance<TeamCharacterDataInfo>();
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<TeamCharacterDataInfo>();
mapped2.CharacterID = ah2.ally_heroID;
mapped2.CharacterName = ah2.ally_heroName;
mapped2.CharacterCardSprite = ah2.ally_heroImage;
mapped2.CharacterTeamSprite = ah2.ally_heroProfile;
found = mapped2; break;
}
}
// ˵ȫ teamCharacterList
if (found == null && globalList != null && globalList.characters != null)
{
for (int j = 0; j < globalList.characters.Count; j++)
{
var so = globalList.characters[j];
if (so == null) continue;
if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString()))
{
found = so;
break;
}
if (so.CharacterID == id)
{
found = so;
break;
}
}
}
// final fallback: try loading by resource path/id if resourcesPath provided
if (found == null && !string.IsNullOrEmpty(resourcesPath))
{
try
{
var byId = Resources.Load<TeamCharacterDataInfo>($"{resourcesPath}/{id}");
if (byId != null) found = byId;
if (found != null) runtimeSourceInfo = $"Resources.Load: {resourcesPath}/{id}";
}
catch { }
}
currentAllySOs[i] = found;
if (found == null)
{
Debug.LogWarning($"[teamUIController] (Runtime) δҵ id={id} Ӧ SO (slot {i + 1})");
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
ToggleAllyObject(i, false);
}
else
{
// try to get a readable source for the matched object
if (string.IsNullOrEmpty(runtimeSourceInfo))
{
runtimeSourceInfo = found.name + " (mapped)";
}
Debug.Log($"[teamUIcontroller] (Runtime) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID}) Source={runtimeSourceInfo}");
switch (i)
{
case 0: isAlly01_active = true; break;
case 1: isAlly02_active = true; break;
case 2: isAlly03_active = true; break;
case 3: isAlly04_active = true; break;
case 4: isAlly05_active = true; break;
}
ToggleAllyObject(i, true);
DebugCharacterSO(found, i+1, false);
// attempt to set teammate image from underlying AllyHero_SO if possible
try
{
var ahList = Resources.LoadAll<AllyHero_SO>("");
AllyHero_SO matched = null;
foreach (var a in ahList) if (a != null && a.ally_heroID == id) { matched = a; break; }
if (matched != null)
{
var sprite = matched.ally_heroProfile != null ? matched.ally_heroProfile : matched.ally_heroImage;
SetTeammateCharacterImage(i, sprite);
}
else
{
var tc = found as TeamCharacterDataInfo;
if (tc != null && tc.CharacterTeamSprite != null)
SetTeammateCharacterImage(i, tc.CharacterTeamSprite);
}
}
catch { }
}
}
#endif
}
// ResolveResourcesRelativePath helper method is missing, assuming it exists externally or is part of a base class / extension.
// Since the user is providing the full class, I will assume it's either defined outside this snippet or is implicitly handled,
// but the code within #if UNITY_EDITOR is what matters for the file content.
// For completeness, if it's not defined, the runtime block would throw an error, but as I cannot add it, I must assume it's defined elsewhere.
// A simplified placeholder for ResolveResourcesRelativePath for compilation (assuming it strips Resources/ and leading/trailing slashes):
private string ResolveResourcesRelativePath(string path)
{
if (string.IsNullOrEmpty(path)) return string.Empty;
path = path.Replace("\\", "/");
path = path.Replace("Assets/Resources/", "");
path = path.Replace("Resources/", "");
return path.Trim('/').ToLower();
}
// Populate enemy SOs from enemySlotIds using editor/runtime paths similar to allies
public void PopulateEnemySOsFromIds()
{
Debug.Log($"PopulateEnemySOsFromIds called, enemySlotIds: {string.Join(",", enemySlotIds)}");
if (enemySlotIds == null || enemySlotIds.Count == 0)
{
currentEnemySOs = new EnemyData_SO[0];
// update recognized list and UI
FilterRecognizedEnemies();
UpdateEnemyListText();
return;
}
#if UNITY_EDITOR
string folderToUse = !string.IsNullOrEmpty(editorEnemySOFolderPath) ? editorEnemySOFolderPath : selectedProjectFolderPath;
string projectRelative = null;
if (!string.IsNullOrEmpty(folderToUse))
{
string projectPath = folderToUse.Replace("\\", "/");
if (projectPath.StartsWith(Application.dataPath)) projectRelative = "Assets" + projectPath.Substring(Application.dataPath.Length);
else if (projectPath.StartsWith("Assets/")) projectRelative = projectPath;
else Debug.LogWarning("editorEnemySOFolderPath should be inside project Assets for editor search. Falling back to project-wide search.");
}
List<EnemyData_SO> results = new List<EnemyData_SO>();
List<Object> discovered = new List<Object>();
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<EnemyData_SO>(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<EnemyData_SO>(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<Object> runtimeDiscovered = new List<Object>();
if (!string.IsNullOrEmpty(resourcesPath))
{
resourcesPath = resourcesPath.Trim('/');
try
{
var arr = Resources.LoadAll(resourcesPath);
if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr);
if (runtimeDiscovered.Count == 0)
{
var arrAll = Resources.LoadAll("");
if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); // Fixed: arr.Length to arrAll.Length
}
}
catch { var arrAll = Resources.LoadAll(""); if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); } // Fixed: arr.Length to arrAll.Length
}
else
{
var arr = Resources.LoadAll(""); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr);
}
List<EnemyData_SO> results = new List<EnemyData_SO>();
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<EnemyData_SO>($"{resourcesPath}/{id}");
if (byId != null) found = byId;
}
catch { }
}
results.Add(found);
}
currentEnemySOs = results.ToArray();
#endif
// build recognized list and update UI
FilterRecognizedEnemies();
UpdateEnemyListText();
// Calculate total max HP for all recognized enemies
// Note: totalMaxHP is a class member and is intended to be the fixed maximum HP for the entire encounter.
RecalculateTotalMaxHP();
if (recognizedEnemySOs.Length > 0)
{
enemyCurrentCount = 0;
SpawnNextEnemy();
}
// If enemies were populated and we have some, start spawning from the beginning
if (recognizedEnemySOs.Length > 0)
{
enemyCurrentCount = 0;
SpawnNextEnemy();
}
}
// Build recognizedEnemySOs from currentEnemySOs by filtering out null entries (id==0 or unresolved)
private void FilterRecognizedEnemies()
{
var list = new List<EnemyData_SO>();
if (currentEnemySOs != null)
{
foreach (var so in currentEnemySOs)
{
if (so != null)
{
list.Add(so);
}
}
}
recognizedEnemySOs = list.ToArray();
// update enemyCounterMax to reflect actual number of recognized enemies
enemyCounterMax = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0;
// clamp current index
if (enemyCurrentCount < 0) enemyCurrentCount = 0;
if (enemyCurrentCount > enemyCounterMax) enemyCurrentCount = enemyCounterMax;
}
// ؽĵ SO б
public EnemyData_SO[] GetCurrentEnemySOs()
{
return (EnemyData_SO[])currentEnemySOs.Clone();
}
// Debug helper to print key fields of a TeamCharacterDataInfo
private void DebugCharacterSO(TeamCharacterDataInfo so, int slotIndex, bool isEditor)
{
if (so == null) return;
string mode = isEditor ? "Editor" : "Runtime";
string spriteCard = so.CharacterCardSprite != null ? so.CharacterCardSprite.name : "(null)";
string spriteTeam = so.CharacterTeamSprite != null ? so.CharacterTeamSprite.name : "(null)";
Debug.Log($"[teamUIController] ({mode}) Slot {slotIndex} -> CharacterID={so.CharacterID}, Name={so.CharacterName}, Skill={so.CharacterSkillName}, MaxHealth={so.CharacterMaxHealth}, CardSprite={spriteCard}, TeamSprite={spriteTeam}");
}
/// <summary>
/// صǰ SO б
/// </summary>
public TeamCharacterDataInfo[] GetCurrentAllySOs()
{
return (TeamCharacterDataInfo[])currentAllySOs.Clone();
}
[Header("ı")]
[Tooltip("title")]
public TextMeshProUGUI songNametitle;
public TextMeshProUGUI difficultyName;
public TextMeshProUGUI difficultyID;
public TextMeshProUGUI constructionName;
[Tooltip("")]
public Text comboCounter;
[Tooltip("˳")]
public Text enemyCounter;
[SerializeField] private int enemyCounterMax; // Ϸ
[SerializeField] private int enemyCurrentCount; // ڵĵ˳
[Tooltip("ǰܷ")]
public TextMeshProUGUI currentTotalScore;
[Header("ܷ")]
public TextMeshProUGUI allSum_pmScore;
[Header("ܷ")]
public TextMeshProUGUI allSum_idolScore;
// new: runtime enemy instance and UI sync fields
private EnemyCombatant enemyCombatantInstance;
private Coroutine enemyFadeHealthCoroutine;
private Coroutine enemyFadeManaCoroutine;
private int prevEnemyHP = -1;
private int prevEnemyMana = -1;
// For total enemy health bar
private int totalMaxHP = 0; // totalMaxHP
private Coroutine totalFadeHealthCoroutine;
// Ƴ private float prevTotalHealthFill = 1f;
[Header("ComboжҪ")]
public ComboJudgeType comboJudgeType = ComboJudgeType.Perfect;
private int combo = 0;
public enum ComboJudgeType
{
Perfect,
Great,
Good,
// Miss
}
[Header("teammate01")]
[Tooltip("-")]
public GameObject objectFather_ally01;
[Tooltip("")]
public bool isAlly01_active = true;
[Tooltip("ɫͼ")]
public Image teammate01_characterImage;
[Tooltip("Ѫ")]
public Image teammate01_healthImage;
[Tooltip("Ѫ")]
public Image teammate01_fadehealthImage;
[Tooltip("")]
public Image teammate01_manaImage;
[Tooltip("")]
public Image teammate01_fadeManaImage;
[Tooltip("ı")]
public TextMeshProUGUI teammate01_nameText;
[Tooltip("ǰֵ/ֵ")]
public TextMeshProUGUI teammate01_healthRate;
[Tooltip("ǰ/")]
public TextMeshProUGUI teammate01_current_scoreText; // rate : now score / max score
[Tooltip("ɫΨһʶ")]
[SerializeField] private int ally01_id;
[Tooltip("ܻ")]
public Image teammate01_hurtRedImage;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate01_currentHP;
[Tooltip("ֵ")]
[SerializeField] private int teammate01_maxHP;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate01_currentMana;
[Tooltip("ֵ")]
[SerializeField] private int teammate01_maxMana;
[Header("pmScore")]
public TextMeshProUGUI red_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI red_idolScore_sum;
[Header("teammate02")]
[Tooltip("-")]
public GameObject objectFather_ally02;
[Tooltip("")]
public bool isAlly02_active = true;
[Tooltip("ɫͼ")]
public Image teammate02_characterImage;
[Tooltip("Ѫ")]
public Image teammate02_healthImage;
[Tooltip("Ѫ")]
public Image teammate02_fadehealthImage;
[Tooltip("")]
public Image teammate02_manaImage;
[Tooltip("")]
public Image teammate02_fadeManaImage;
[Tooltip("ı")]
public TextMeshProUGUI teammate02_nameText;
[Tooltip("ǰֵ/ֵ")]
public TextMeshProUGUI teammate02_healthRate;
[Tooltip("ǰ/")]
public TextMeshProUGUI teammate02_current_scoreText; // rate : now score / max score
[Tooltip("ɫΨһʶ")]
[SerializeField] private int ally02_id;
[Tooltip("ܻ")]
public Image teammate02_hurtRedImage;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate02_currentHP;
[Tooltip("ֵ")]
[SerializeField] private int teammate02_maxHP;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate02_currentMana;
[Tooltip("ֵ")]
[SerializeField] private int teammate02_maxMana;
[Header("pmScore")]
public TextMeshProUGUI green_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI green_idolScore_sum;
[Header("teammate03")]
[Tooltip("-")]
public GameObject objectFather_ally03;
[Tooltip("")]
public bool isAlly03_active = true;
[Tooltip("ɫͼ")]
public Image teammate03_characterImage;
[Tooltip("Ѫ")]
public Image teammate03_healthImage;
[Tooltip("Ѫ")]
public Image teammate03_fadehealthImage;
[Tooltip("")]
public Image teammate03_manaImage;
[Tooltip("")]
public Image teammate03_fadeManaImage;
[Tooltip("ı")]
public TextMeshProUGUI teammate03_nameText;
[Tooltip("ǰֵ/ֵ")]
public TextMeshProUGUI teammate03_healthRate;
[Tooltip("ǰ/")]
public TextMeshProUGUI teammate03_current_scoreText; // rate : now score / max score
[Tooltip("ɫΨһʶ")]
[SerializeField] private int ally03_id;
[Tooltip("ܻ")]
public Image teammate03_hurtRedImage;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate03_currentHP;
[Tooltip("ֵ")]
[SerializeField] private int teammate03_maxHP;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate03_currentMana;
[Tooltip("ֵ")]
[SerializeField] private int teammate03_maxMana;
[Header("pmScore")]
public TextMeshProUGUI yellow_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI yellow_idolScore_sum;
[Header("teammate04")]
[Tooltip("-")]
public GameObject objectFather_ally04;
[Tooltip("")]
public bool isAlly04_active = true;
[Tooltip("ɫͼ")]
public Image teammate04_characterImage;
[Tooltip("Ѫ")]
public Image teammate04_healthImage;
[Tooltip("Ѫ")]
public Image teammate04_fadehealthImage;
[Tooltip("")]
public Image teammate04_manaImage;
[Tooltip("")]
public Image teammate04_fadeManaImage;
[Tooltip("ı")]
public TextMeshProUGUI teammate04_nameText;
[Tooltip("ǰֵ/ֵ")]
public TextMeshProUGUI teammate04_healthRate;
[Tooltip("ǰ/")]
public TextMeshProUGUI teammate04_current_scoreText; // rate : now score / max score
[Tooltip("ɫΨһʶ")]
[SerializeField] private int ally04_id;
[Tooltip("ܻ")]
public Image teammate04_hurtRedImage;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate04_currentHP;
[Tooltip("ֵ")]
[SerializeField] private int teammate04_maxHP;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate04_currentMana;
[Tooltip("ֵ")]
[SerializeField] private int teammate04_maxMana;
[Header("pmScore")]
public TextMeshProUGUI purple_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI purple_idolScore_sum;
[Header("teammate05")]
[Tooltip("-")]
public GameObject objectFather_ally05;
[Tooltip("")]
public bool isAlly05_active = true;
[Tooltip("ɫͼ")]
public Image teammate05_characterImage;
[Tooltip("Ѫ")]
public Image teammate05_healthImage;
[Tooltip("Ѫ")]
public Image teammate05_fadehealthImage;
[Tooltip("")]
public Image teammate05_manaImage;
[Tooltip("")]
public Image teammate05_fadeManaImage;
[Tooltip("ı")]
public TextMeshProUGUI teammate05_nameText;
[Tooltip("ǰֵ/ֵ")]
public TextMeshProUGUI teammate05_healthRate;
[Tooltip("ǰ/")]
public TextMeshProUGUI teammate05_current_scoreText; // rate : now score / max score
[Tooltip("ɫΨһʶ")]
[SerializeField] private int ally05_id;
[Tooltip("ܻ")]
public Image teammate05_hurtRedImage;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate05_currentHP;
[Tooltip("ֵ")]
[SerializeField] private int teammate05_maxHP;
[Tooltip("ǰֵ")]
[SerializeField] private int teammate05_currentMana;
[Tooltip("ֵ")]
[SerializeField] private int teammate05_maxMana;
[Header("pmScore")]
public TextMeshProUGUI blue_pmScore_sum;
[Header("idolScore")]
public TextMeshProUGUI blue_idolScore_sum;
[Header("currentEnemy")]
[Tooltip("-")]
public GameObject objectFather_enemy;
[Tooltip("")]
public bool isEnemy_active = true;
[Tooltip("ɫͼ")]
public Image currentEnemy_characterImage;
[Tooltip("Ѫ")]
public Image currentEnemy_healthImage;
[Tooltip("Ѫ")]
public Image currentEnemy_fadehealthImage;
[Tooltip("")]
public Image currentEnemy_manaImage;
[Tooltip("")]
public Image currentEnemy_fademanaImage;
[Tooltip("Ѫ")]
public Image allEnemy_totalHealthImage;
[Tooltip("Ѫ")]
public Image allEnemy_totalFadehealthImage;
[Tooltip("ı")]
public Text currentEnemy_nameText;
[Tooltip("ı")]
public Text currentEnemy_typeText;
[Tooltip("ǰֵ/ֵ")]
public TextMeshProUGUI currentEnemy_healthRate;
[Tooltip("еֵ/ֵܼ")]
public TextMeshProUGUI totalEnemy_healthRate;
[Tooltip("ǰ˷ֵ/ֵ")]
public TextMeshProUGUI currentEnemy_manaRate;
[Tooltip("ǰ/")]
public TextMeshProUGUI currentEnemy_current_scoreText; // rate : now score / max score
[Tooltip("Ψһʶ")]
[SerializeField] private int enemy_id;
[Tooltip("ܻ")]
public Image currentEnemy_hurtRedImage;
[Tooltip("Ŷ")]
public TextMeshProUGUI enemyList_rateText;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
// Load ally slot IDs from PlayerPrefs
allySlotIds[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
allySlotIds[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
allySlotIds[2] = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
allySlotIds[3] = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
allySlotIds[4] = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
PopulateAllySOsFromIds();
// initialize previous flags so Update can detect changes
prevAllyActive[0] = isAlly01_active;
prevAllyActive[1] = isAlly02_active;
prevAllyActive[2] = isAlly03_active;
prevAllyActive[3] = isAlly04_active;
prevAllyActive[4] = isAlly05_active;
// Ensure score text references are resolved so ScoreManager and AllyCombatant can write to them
ResolveScoreTextReferences();
// Initialize enemies list and spawn first enemy if any
PopulateEnemySOsFromIds();
// set enemyCounterMax based on recognized (resolved non-null) enemies so zeros/unresolved don't count
enemyCounterMax = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0;
enemyCurrentCount = 0;
InitializeEnemyInstance();
if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0)
{
SpawnNextEnemy();
}
}
// Update is called once per frame
void Update()
{
// detect external changes to isAllyX_active and toggle objects accordingly
bool[] current = new bool[] { isAlly01_active, isAlly02_active, isAlly03_active, isAlly04_active, isAlly05_active };
for (int i = 0; i < 5; i++)
{
if (prevAllyActive[i] != current[i])
{
ToggleAllyObject(i, current[i]);
prevAllyActive[i] = current[i];
}
}
// Sync enemy UI with EnemyCombatant values
if (enemyCombatantInstance != null)
{
int hp = enemyCombatantInstance.currentHP;
int mana = enemyCombatantInstance.currentMana;
if (prevEnemyHP != hp)
{
UpdateEnemyHealthVisuals(prevEnemyHP, hp, true);
// ǰ˵ HP 仯ʱҲҪѪ
UpdateAllEnemyTotalHealthUIOnHpChange(hp);
prevEnemyHP = hp;
}
if (prevEnemyMana != mana)
{
UpdateEnemyManaVisuals(prevEnemyMana, mana, true);
prevEnemyMana = mana;
}
}
}
private void FixedUpdate()
{
}
private void Awake()
{
if (Instance == null)
Instance = this;
else
Destroy(gameObject);
}
/// <summary>
/// жϵͳãж"Perfect""Great""Good""Miss"
/// </summary>
public void OnJudgeResult(string result)
{
if (IsCombo(result))
{
combo++;
}
else
{
combo = 0;
}
if (comboCounter != null)
comboCounter.text = combo.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.");
}
}
}
/// <summary>
/// жϵǰжǷﵽcomboҪ
/// </summary>
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<TextMeshProUGUI>(true);
if (tmp != null) return tmp;
// Legacy Text fallback: copy text to a new TMP component on the same GameObject
var legacy = candidate.GetComponentInChildren<Text>(true);
if (legacy != null)
{
var go = legacy.gameObject;
var added = go.GetComponent<TextMeshProUGUI>() ?? go.AddComponent<TextMeshProUGUI>();
added.text = legacy.text;
// Optionally disable legacy renderer to avoid duplicate rendering
legacy.enabled = false;
Debug.LogWarning($"[teamUIController] Converted legacy Text to TMP on {go.name} (parent {candidate.name})");
return added;
}
}
// If candidate not found or no components under it, try direct scene object by name
if (!string.IsNullOrEmpty(sceneNameHint))
{
var goByName = GameObject.Find(sceneNameHint);
if (goByName != null)
{
var tmp2 = goByName.GetComponent<TextMeshProUGUI>();
if (tmp2 != null) return tmp2;
var legacy2 = goByName.GetComponent<Text>();
if (legacy2 != null)
{
var created = goByName.GetComponent<TextMeshProUGUI>() ?? goByName.AddComponent<TextMeshProUGUI>();
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<TextMeshProUGUI>(true);
if (childTmp != null) return childTmp;
var childLegacy = goByName.GetComponentInChildren<Text>(true);
if (childLegacy != null)
{
var go = childLegacy.gameObject;
var created = go.GetComponent<TextMeshProUGUI>() ?? go.AddComponent<TextMeshProUGUI>();
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<TextMeshProUGUI>();
var lowHint = sceneNameHint.ToLower();
foreach (var t in all)
{
if (t == null || t.gameObject == null) continue;
if (t.gameObject.name.ToLower().Contains(lowHint) || t.gameObject.name.ToLower().Contains("pm") && t.gameObject.name.ToLower().Contains(lowHint))
return t;
}
}
return null;
}
// Helper: try resolve TMP under parent, then scene by name
void TryResolveScore(ref TextMeshProUGUI field, GameObject parent, string sceneName)
{
if (field != null) return;
// 1) under parent
field = FindOrConvertTMP(parent, null);
if (field != null)
{
Debug.Log($"[teamUIController] Resolved score TMP from parent {parent?.name} -> {field.gameObject.name}");
return;
}
// 2) by scene object name
field = FindOrConvertTMP(null, sceneName);
if (field != null)
{
Debug.Log($"[teamUIController] Resolved score TMP by scene name '{sceneName}' -> {field.gameObject.name}");
return;
}
Debug.LogWarning($"[teamUIController] Could not resolve {sceneName} TMP (parent '{parent?.name}')");
}
// Resolve per-track current score TMPs (these are usually set in inspector; fallback to parent lookup)
TryResolveScore(ref teammate01_current_scoreText, objectFather_ally01, "teammate01_current_scoreText");
TryResolveScore(ref teammate02_current_scoreText, objectFather_ally02, "teammate02_current_scoreText");
TryResolveScore(ref teammate03_current_scoreText, objectFather_ally03, "teammate03_current_scoreText");
TryResolveScore(ref teammate04_current_scoreText, objectFather_ally04, "teammate04_current_scoreText");
TryResolveScore(ref teammate05_current_scoreText, objectFather_ally05, "teammate05_current_scoreText");
// Resolve pm score TMPs and aggregate sum (recommend these fields be TextMeshProUGUI)
TryResolveScore(ref red_pmScore_sum, null, "red_pmScore_sum");
TryResolveScore(ref green_pmScore_sum, null, "green_pmScore_sum");
TryResolveScore(ref yellow_pmScore_sum, null, "yellow_pmScore_sum");
TryResolveScore(ref purple_pmScore_sum, null, "purple_pmScore_sum");
TryResolveScore(ref blue_pmScore_sum, null, "blue_pmScore_sum");
// allSum_pmScore may have been declared as TextMeshProUGUI; if it's legacy Text, convert similarly
// If you declared allSum_pmScore as TextMeshProUGUI field, do:
TryResolveScore(ref allSum_pmScore, null, "allSum_pmScore");
// total score: if assigned field is null, try to find TMP child under this object or by name "currentTotalScore"
if (currentTotalScore == null)
{
var tmp = GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
{
currentTotalScore = tmp;
Debug.Log($"[teamUIController] Resolved currentTotalScore from child {tmp.gameObject.name}");
}
else
{
var byName = GameObject.Find("currentTotalScore");
if (byName != null)
{
var t = byName.GetComponent<TextMeshProUGUI>() ?? byName.GetComponent<Text>()?.gameObject.AddComponent<TextMeshProUGUI>();
if (t != null) currentTotalScore = t;
}
}
}
}
/// <summary>
/// ڵѾңֻ߲һ
/// </summary>
public int[] GetAdjacentAllyIndices(int slotIndex)
{
var list = new List<int>();
if (slotIndex - 1 >= 0) list.Add(slotIndex - 1);
if (slotIndex + 1 <= 4) list.Add(slotIndex + 1);
return list.ToArray();
}
/// <summary>
/// ָλӦѾ GameObjectȷ allyX_object򷵻 objectFather_allyXXΪ null
/// </summary>
public GameObject GetAllyObjectBySlot(int slotIndex)
{
switch (slotIndex)
{
case 0: return ally01_object ?? objectFather_ally01;
case 1: return ally02_object ?? objectFather_ally02;
case 2: return ally03_object ?? objectFather_ally03;
case 3: return ally04_object ?? objectFather_ally04;
case 4: return ally05_object ?? objectFather_ally05;
default: return null;
}
}
// helper to set teammate character image based on slot index
private void SetTeammateCharacterImage(int slotIndex, Sprite sprite)
{
if (sprite == null) return;
switch (slotIndex)
{
case 0: if (teammate01_characterImage != null) teammate01_characterImage.sprite = sprite; break;
case 1: if (teammate02_characterImage != null) teammate02_characterImage.sprite = sprite; break;
case 2: if (teammate03_characterImage != null) teammate03_characterImage.sprite = sprite; break;
case 3: if (teammate04_characterImage != null) teammate04_characterImage.sprite = sprite; break;
case 4: if (teammate05_characterImage != null) teammate05_characterImage.sprite = sprite; break;
}
}
// ------------------- Enemy spawn & UI sync helpers -------------------
private void InitializeEnemyInstance()
{
// Try to find an existing enemy object in scene
enemyCombatantInstance = FindFirstObjectByType<EnemyCombatant>();
if (enemyCombatantInstance == null)
{
var go = new GameObject("thisEnemy");
enemyCombatantInstance = go.AddComponent<EnemyCombatant>();
}
// attach listeners
if (enemyCombatantInstance != null)
{
enemyCombatantInstance.OnEnemyDied -= OnEnemyDiedHandler;
enemyCombatantInstance.OnEnemyRevived -= OnEnemyRevivedHandler;
enemyCombatantInstance.OnEnemyDied += OnEnemyDiedHandler;
enemyCombatantInstance.OnEnemyRevived += OnEnemyRevivedHandler;
// initialize previous tracked values
prevEnemyHP = enemyCombatantInstance.currentHP;
prevEnemyMana = enemyCombatantInstance.currentMana;
}
}
private void SpawnNextEnemy()
{
// handle case: no configured/recognized enemies -> show empty state but keep UI visible
if (recognizedEnemySOs == null || recognizedEnemySOs.Length == 0)
{
if (objectFather_enemy != null) objectFather_enemy.SetActive(true);
if (currentEnemy_nameText != null) currentEnemy_nameText.text = "(No Enemy)";
if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = 0f;
if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = 0f;
if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = "0/0";
if (currentEnemy_manaImage != null) currentEnemy_manaImage.fillAmount = 0f;
if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = 0f;
if (enemyCounter != null) enemyCounter.text = "0/0";
if (currentEnemy_typeText != null) currentEnemy_typeText.text = "(ͨ)";
UpdateEnemyListText();
// ˢѪûеʱʾ 0/TotalMax
UpdateAllEnemyTotalHealthUIImmediate();
return;
}
if (enemyCurrentCount < 0) enemyCurrentCount = 0;
if (enemyCurrentCount >= recognizedEnemySOs.Length)
{
// All enemies processed: show empty/finished state but keep UI visible
if (objectFather_enemy != null) objectFather_enemy.SetActive(true);
if (currentEnemy_nameText != null) currentEnemy_nameText.text = "(All Defeated)";
if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = 0f;
if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = 0f;
if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = "0/0";
if (currentEnemy_manaImage != null) currentEnemy_manaImage.fillAmount = 0f;
if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = 0f;
if (enemyCounter != null) enemyCounter.text = "0/" + enemyCounterMax.ToString();
if (currentEnemy_typeText != null) currentEnemy_typeText.text = "(ͨ)";
UpdateEnemyListText();
// ˢѪеʱʾ 0/TotalMax
UpdateAllEnemyTotalHealthUIImmediate();
return;
}
var so = recognizedEnemySOs[enemyCurrentCount];
if (so == null)
{
Debug.LogWarning($"[teamUIController] SpawnNextEnemy: SO is null at index {enemyCurrentCount}");
// skip null slots (treated as empty) and advance
enemyCurrentCount++;
SpawnNextEnemy();
return;
}
InitializeEnemyInstance();
enemyCombatantInstance.InitializeFromSO(so);
// set UI visuals (name / sprites)
if (currentEnemy_nameText != null) currentEnemy_nameText.text = so.enemyName ?? so.name;
if (currentEnemy_characterImage != null)
{
var sprite = so.enemy_Profile != null ? so.enemy_Profile : so.enemy_Image;
if (sprite != null) currentEnemy_characterImage.sprite = sprite;
else currentEnemy_characterImage.sprite = null;
}
// set type text: show for Simple/Elite/Boss/Legend
if (currentEnemy_typeText != null)
{
switch (so.enemyType)
{
case EnemyData_SO.EnemyType.Elite:
currentEnemy_typeText.text = "(Ӣ)";
break;
case EnemyData_SO.EnemyType.Boss:
currentEnemy_typeText.text = "()";
break;
case EnemyData_SO.EnemyType.Legend:
currentEnemy_typeText.text = "(ʷʫ)";
break;
default:
currentEnemy_typeText.text = "(ͨ)";
break;
}
}
// ensure UI parent is visible
if (objectFather_enemy != null) objectFather_enemy.SetActive(true);
// reset previous values so Update picks up initial state
prevEnemyHP = enemyCombatantInstance.currentHP;
prevEnemyMana = enemyCombatantInstance.currentMana;
UpdateEnemyUIImmediate();
// update enemy counter text
if (enemyCounter != null)
{
enemyCounter.text = $"{Mathf.Clamp(enemyCurrentCount + 1, 1, 999)}/{enemyCounterMax}";
}
// refresh the enemy list display
UpdateEnemyListText();
}
// Build a readable representation of the enemy queue and write to enemyList_rateText
private void UpdateEnemyListText()
{
if (enemyList_rateText == null) return;
int total = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0;
int displayIndex = 0;
if (total == 0)
displayIndex = 0;
else
displayIndex = Mathf.Clamp(enemyCurrentCount + 1, 1, total);
// show as "current/total" (e.g. 1/3). When no enemies, show 0/0
enemyList_rateText.text = total == 0 ? "0/0" : $"{displayIndex}/{total}";
}
private void OnEnemyDiedHandler(EnemyCombatant e)
{
// advance to next enemy after death
enemyCurrentCount++;
SpawnNextEnemy();
}
private void OnEnemyRevivedHandler(EnemyCombatant e)
{
// refresh UI
UpdateEnemyUIImmediate();
}
// teamUIController.cs -
// =========================================================
// BeatmapManager õķõ HP
// =========================================================
/// <summary>
/// õĵֵдʶĵ Scriptable Object С
/// </summary>
public void ApplyCalculatedEnemyHP(int individualMaxHP)
{
// ȷ recognizedEnemySOs 鲻Ϊ
if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0)
{
Debug.Log($"[teamUIController] Applying calculated HP: {individualMaxHP} to {recognizedEnemySOs.Length} recognized enemies.");
// 1. ޸ SO ݣʶĵ SOֵ
foreach (var so in recognizedEnemySOs)
{
// EnemyData_SO public int enemy_maxHP; ֶ
if (so != null)
{
so.enemy_maxHP = individualMaxHP;
}
}
// 2. ¼ѪѪ
RecalculateTotalMaxHP();
// 3. ǿˢµǰɵĵʵ
// Ϊ Populate Ѿ Apply ֮ǰ˾ݵĵˣҪ
if (enemyCombatantInstance != null && enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length)
{
var currentSO = recognizedEnemySOs[enemyCurrentCount];
if (currentSO != null)
{
Debug.Log($"[teamUIController] Re-initializing current enemy instance with new HP: {individualMaxHP}");
enemyCombatantInstance.InitializeFromSO(currentSO); // ʹµ SO ݳʼʵ
UpdateEnemyUIImmediate(); // ˢ UI
}
}
}
}
// =========================================================
// ֵ¼װԭеۼ߼
// =========================================================
/// <summary>
/// ʶ˵ SOۼǵֵ totalMaxHPˢѪ
/// </summary>
private void RecalculateTotalMaxHP()
{
totalMaxHP = 0;
if (recognizedEnemySOs != null)
{
foreach (var so in recognizedEnemySOs)
{
if (so != null) totalMaxHP += so.enemy_maxHP;
}
}
Debug.Log($"[teamUIController] Total Max HP has been recalculated to: {totalMaxHP}");
// ˢѪʾֹӳ
UpdateAllEnemyTotalHealthUIImmediate();
}
// ڵǰ HP 仯ʱѪ UI
private void UpdateAllEnemyTotalHealthUIOnHpChange(int newCurrentHP)
{
if (allEnemy_totalHealthImage == null) return;
// ¼ܵǰѪcurrentEnemySOs ֲ active enemy current HP ıˣ
int totalCur = 0;
if (currentEnemySOs != null)
{
for (int i = enemyCurrentCount; i < currentEnemySOs.Length; i++)
{
var so = currentEnemySOs[i];
if (so == null) continue;
// µǰԾ˵Ѫ˱ Max HP
if (i == enemyCurrentCount) totalCur += newCurrentHP; else totalCur += so.enemy_maxHP;
}
}
float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f;
// 1. ʵѪ
allEnemy_totalHealthImage.fillAmount = newFill;
if (totalEnemy_healthRate != null) totalEnemy_healthRate.text = $"{totalCur}/{totalMaxHP}";
// 2. Animate fade bar (only when health decreases)
if (allEnemy_totalFadehealthImage != null)
{
float currentFadeFill = allEnemy_totalFadehealthImage.fillAmount;
if (currentFadeFill > newFill)
{
// Health decreased: stop previous coroutine and start new fall animation
if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine);
if (Application.isPlaying)
totalFadeHealthCoroutine = StartCoroutine(TotalFadeHealthCoroutine(newFill));
else
allEnemy_totalFadehealthImage.fillAmount = newFill;
}
else if (currentFadeFill < newFill)
{
// Health increased (healing): update fade bar immediately
if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine);
allEnemy_totalFadehealthImage.fillAmount = newFill;
}
// If currentFadeFill == newFill, do nothing.
}
}
// SpawnNextEnemy/No Enemy ʱѪʹö߼
private void UpdateAllEnemyTotalHealthUIImmediate()
{
if (allEnemy_totalHealthImage == null) return;
int totalCur = 0;
if (currentEnemySOs != null)
{
for (int i = enemyCurrentCount; i < currentEnemySOs.Length; i++)
{
var so = currentEnemySOs[i];
if (so == null) continue;
// µǰԾ˵Ѫ˱ Max HP
if (i == enemyCurrentCount)
{
// ʹ Combatant ʵĵǰ HP
if (enemyCombatantInstance != null) totalCur += enemyCombatantInstance.currentHP;
else totalCur += so.enemy_maxHP; // Fallback
}
else
{
totalCur += so.enemy_maxHP;
}
}
}
float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f;
// ʵѪѪ
allEnemy_totalHealthImage.fillAmount = newFill;
if (allEnemy_totalFadehealthImage != null) allEnemy_totalFadehealthImage.fillAmount = newFill;
if (totalEnemy_healthRate != null) totalEnemy_healthRate.text = $"{totalCur}/{totalMaxHP}";
// ֹͣеѪ
if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine);
}
private void UpdateEnemyUIImmediate()
{
if (enemyCombatantInstance == null) return;
// health
if (currentEnemy_healthImage != null)
currentEnemy_healthImage.fillAmount = enemyCombatantInstance.maxHP > 0 ? (float)enemyCombatantInstance.currentHP / enemyCombatantInstance.maxHP : 0f;
if (currentEnemy_fadehealthImage != null)
currentEnemy_fadehealthImage.fillAmount = currentEnemy_healthImage != null ? currentEnemy_healthImage.fillAmount : (enemyCombatantInstance.maxHP > 0 ? (float)enemyCombatantInstance.currentHP / enemyCombatantInstance.maxHP : 0f);
if (currentEnemy_healthRate != null)
currentEnemy_healthRate.text = $"{enemyCombatantInstance.currentHP}/{enemyCombatantInstance.maxHP}";
// mana visuals (new)
if (currentEnemy_manaImage != null)
currentEnemy_manaImage.fillAmount = enemyCombatantInstance.maxMana > 0 ? (float)enemyCombatantInstance.currentMana / enemyCombatantInstance.maxMana : 0f;
if (currentEnemy_fademanaImage != null)
currentEnemy_fademanaImage.fillAmount = currentEnemy_manaImage != null ? currentEnemy_manaImage.fillAmount : (enemyCombatantInstance.maxMana > 0 ? (float)enemyCombatantInstance.currentMana / enemyCombatantInstance.maxMana : 0f);
if (currentEnemy_manaRate != null)
currentEnemy_manaRate.text = enemyCombatantInstance.maxMana > 0 ? $"{enemyCombatantInstance.currentMana}/{enemyCombatantInstance.maxMana}" : "0/0";
// Aggregate total health logic (modified to call the helper method for animation control)
UpdateAllEnemyTotalHealthUIImmediate();
}
private void UpdateEnemyHealthVisuals(int oldHP, int newHP, bool animateFade)
{
if (enemyCombatantInstance == null) return;
if (currentEnemy_healthImage != null)
{
float newFill = enemyCombatantInstance.maxHP > 0 ? (float)newHP / enemyCombatantInstance.maxHP : 0f;
currentEnemy_healthImage.fillAmount = Mathf.MoveTowards(currentEnemy_healthImage.fillAmount, newFill, 1f);
}
if (currentEnemy_healthRate != null)
currentEnemy_healthRate.text = $"{newHP}/{enemyCombatantInstance.maxHP}";
if (enemyFadeHealthCoroutine != null) StopCoroutine(enemyFadeHealthCoroutine);
if (currentEnemy_fadehealthImage != null)
{
float target = currentEnemy_healthImage != null ? currentEnemy_healthImage.fillAmount : (enemyCombatantInstance.maxHP > 0 ? (float)newHP / enemyCombatantInstance.maxHP : 0f);
if (animateFade && Application.isPlaying)
enemyFadeHealthCoroutine = StartCoroutine(EnemyFadeHealthCoroutine(target));
else
currentEnemy_fadehealthImage.fillAmount = target;
}
// hurt flash if hp decreased
if (oldHP >= 0 && newHP < oldHP && currentEnemy_hurtRedImage != null && Application.isPlaying)
{
StopCoroutine("EnemyHurtFlash");
StartCoroutine(EnemyHurtFlash());
}
}
private void UpdateEnemyManaVisuals(int oldMana, int newMana, bool animateFade)
{
if (enemyCombatantInstance == null) return;
if (currentEnemy_manaRate != null)
currentEnemy_manaRate.text = enemyCombatantInstance.maxMana > 0 ? $"{newMana}/{enemyCombatantInstance.maxMana}" : "0/0";
if (enemyFadeManaCoroutine != null) StopCoroutine(enemyFadeManaCoroutine);
if (currentEnemy_fademanaImage != null)
{
float target = currentEnemy_manaImage != null ? currentEnemy_manaImage.fillAmount : (enemyCombatantInstance.maxMana > 0 ? (float)newMana / enemyCombatantInstance.maxMana : 0f);
if (animateFade && Application.isPlaying)
enemyFadeManaCoroutine = StartCoroutine(EnemyFadeManaCoroutine(target));
else if (currentEnemy_fademanaImage != null)
currentEnemy_fademanaImage.fillAmount = target;
}
// update main mana bar immediately
if (currentEnemy_manaImage != null)
{
currentEnemy_manaImage.fillAmount = enemyCombatantInstance.maxMana > 0 ? (float)newMana / enemyCombatantInstance.maxMana : 0f;
}
}
private IEnumerator EnemyFadeHealthCoroutine(float targetFill)
{
if (currentEnemy_fadehealthImage == null) yield break;
float start = currentEnemy_fadehealthImage.fillAmount;
float duration = 0.6f;
float t = 0f;
while (t < duration)
{
t += Time.deltaTime;
currentEnemy_fadehealthImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration);
yield return null;
}
currentEnemy_fadehealthImage.fillAmount = targetFill;
}
private IEnumerator EnemyFadeManaCoroutine(float targetFill)
{
if (currentEnemy_fademanaImage == null) yield break;
float start = currentEnemy_fademanaImage.fillAmount;
float duration = 0.4f;
float t = 0f;
while (t < duration)
{
t += Time.deltaTime;
currentEnemy_fademanaImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration);
yield return null;
}
currentEnemy_fademanaImage.fillAmount = targetFill;
}
private IEnumerator EnemyHurtFlash()
{
if (currentEnemy_hurtRedImage == null) yield break;
Color c = currentEnemy_hurtRedImage.color;
c.a = 1f;
currentEnemy_hurtRedImage.color = c;
yield return new WaitForSeconds(0.2f);
float dur = 0.4f;
float t = 0f;
while (t < dur)
{
t += Time.deltaTime;
c.a = Mathf.Lerp(1f, 0f, t / dur);
currentEnemy_hurtRedImage.color = c;
yield return null;
}
c.a = 0f;
currentEnemy_hurtRedImage.color = c;
}
private IEnumerator TotalFadeHealthCoroutine(float targetFill)
{
if (allEnemy_totalFadehealthImage == null) yield break;
float start = allEnemy_totalFadehealthImage.fillAmount;
float duration = 0.6f;
float t = 0f;
while (t < duration)
{
t += Time.deltaTime;
allEnemy_totalFadehealthImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration);
yield return null;
}
allEnemy_totalFadehealthImage.fillAmount = targetFill;
}
// 1. ͨõ˸߼ܻѪ̵ȣ
// 1. 通用的图像闪烁效果(受击闪红、回血闪绿、回蓝闪蓝等)
// 1. 通用的图像闪烁效果(受击闪红、回血闪绿、回蓝闪蓝等)
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(0.2f);
float dur = 0.4f;
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;
}
// 2. ڰ׽߼
// 2. 死亡黑白渐变逻辑
public IEnumerator AllyDeathGrayscale(Image characterImage)
{
if (characterImage == null || grayScaleMaterial == null) yield break;
// 创建材质实例以避免影响其他使用相同材质的物体
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);
}
// ʵȾԭʼ
}