1380 lines
56 KiB
C#
1380 lines
56 KiB
C#
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>();
|
||
|
||
// --- 新增:解析后的当前五个 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 / selectedProjectFolderPath;Runtime: 使用 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 && arr.Length > 0)
|
||
{
|
||
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 && arr.Length > 0)
|
||
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
|
||
}
|
||
|
||
// Populate enemy SOs from enemySlotIds using editor/runtime paths similar to allies
|
||
public void PopulateEnemySOsFromIds()
|
||
{
|
||
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);
|
||
}
|
||
}
|
||
catch { var arrAll = Resources.LoadAll(""); if (arrAll != null && arrAll.Length > 0) runtimeDiscovered.AddRange(arrAll); }
|
||
}
|
||
else
|
||
{
|
||
var arr = Resources.LoadAll(""); if (arr != null && arr.Length > 0) runtimeDiscovered.AddRange(arr);
|
||
}
|
||
|
||
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 (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();
|
||
}
|
||
|
||
// 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;
|
||
|
||
// 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;
|
||
|
||
[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("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("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("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("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("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()
|
||
{
|
||
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);
|
||
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
|
||
private void ResolveScoreTextReferences()
|
||
{
|
||
void TryResolve(ref TextMeshProUGUI field, GameObject parent, string slotName)
|
||
{
|
||
if (field != null) return;
|
||
if (parent != null)
|
||
{
|
||
var tmp = parent.GetComponentInChildren<TextMeshProUGUI>(true);
|
||
if (tmp != null)
|
||
{
|
||
field = tmp;
|
||
Debug.Log($"[teamUIController] Resolved {slotName} TMP from parent {parent.name} -> {tmp.gameObject.name}");
|
||
return;
|
||
}
|
||
var legacy = parent.GetComponentInChildren<Text>(true);
|
||
if (legacy != null)
|
||
{
|
||
// if only legacy Text exists, try to create a TMP component to avoid type mismatch
|
||
var go = legacy.gameObject;
|
||
var created = go.GetComponent<TextMeshProUGUI>() ?? go.AddComponent<TextMeshProUGUI>();
|
||
created.text = legacy.text;
|
||
field = created;
|
||
Debug.LogWarning($"[teamUIController] Found legacy Text for {slotName} under {parent.name}. Added/used TMP component on {go.name} and copied text.");
|
||
return;
|
||
}
|
||
}
|
||
Debug.LogWarning($"[teamUIController] Could not resolve {slotName} TMP (parent '{parent?.name}')");
|
||
}
|
||
|
||
TryResolve(ref teammate01_current_scoreText, objectFather_ally01, "teammate01_current_scoreText");
|
||
TryResolve(ref teammate02_current_scoreText, objectFather_ally02, "teammate02_current_scoreText");
|
||
TryResolve(ref teammate03_current_scoreText, objectFather_ally03, "teammate03_current_scoreText");
|
||
TryResolve(ref teammate04_current_scoreText, objectFather_ally04, "teammate04_current_scoreText");
|
||
TryResolve(ref teammate05_current_scoreText, objectFather_ally05, "teammate05_current_scoreText");
|
||
|
||
// total score: if assigned field is null, try to find any TMP in scene named "currentTotalScore" or under this object
|
||
if (currentTotalScore == null)
|
||
{
|
||
var tmp = GetComponentInChildren<TextMeshProUGUI>(true);
|
||
if (tmp != null)
|
||
{
|
||
currentTotalScore = tmp;
|
||
Debug.Log($"[teamUIController] Resolved currentTotalScore from child {tmp.gameObject.name}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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();
|
||
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();
|
||
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();
|
||
}
|
||
|
||
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";
|
||
|
||
if (allEnemy_totalHealthImage != null)
|
||
{
|
||
// optional: aggregate total health across remaining enemies
|
||
int totalCur = 0, totalMax = 0;
|
||
if (currentEnemySOs != null)
|
||
{
|
||
for (int i = enemyCurrentCount; i < currentEnemySOs.Length; i++)
|
||
{
|
||
var so = currentEnemySOs[i];
|
||
if (so == null) continue;
|
||
totalMax += Mathf.Max(1, so.enemy_maxHP);
|
||
// use max for not-yet-spawned enemies
|
||
if (i == enemyCurrentCount) totalCur += enemyCombatantInstance.currentHP; else totalCur += so.enemy_maxHP;
|
||
}
|
||
}
|
||
if (totalMax > 0)
|
||
{
|
||
allEnemy_totalHealthImage.fillAmount = (float)totalCur / totalMax;
|
||
if (totalEnemy_healthRate != null) totalEnemy_healthRate.text = $"{totalCur}/{totalMax}";
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
} |