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

1618 lines
67 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("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("Editor folder load (Editor only)")]
[Tooltip("Deprecated: kept for compatibility. Use editorSOFolderPath or runtimeResourcesFolderPath instead.")]
public string selectedProjectFolderPath = string.Empty;
/// <summary>
/// 在运行/编辑器中调用:根据 allySlotIds(5 个 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.
totalMaxHP = 0;
if (recognizedEnemySOs != null)
{
foreach (var so in recognizedEnemySOs)
{
if (so != null) totalMaxHP += so.enemy_maxHP;
}
}
// 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;
// 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("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("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("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("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("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();
}
/// <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();
}
// 新增:在当前敌人 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;
}
}