2916 lines
118 KiB
C#
2916 lines
118 KiB
C#
using UnityEngine;
|
||
using Spine;
|
||
using Spine.Unity;
|
||
using SmoothShakeFree;
|
||
using UnityEngine.UI;
|
||
using TMPro;
|
||
using System.Collections.Generic;
|
||
using System.Collections;
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
#endif
|
||
|
||
public class teamUIController : MonoBehaviour
|
||
{
|
||
private static readonly bool VerboseLogs = false;
|
||
private static string NoEnemyLabel => LocalizationService.Get("enemy.none", "(No Enemy)");
|
||
private static string EnemyDefeatedLabel => LocalizationService.Get("enemy.all_defeated", "所有敌人已被击败");
|
||
private static string EnemyTypeNormalLabel => LocalizationService.Get("enemy.type.normal", "(普通敌人)");
|
||
private static string EnemyTypeEliteLabel => LocalizationService.Get("enemy.type.elite", "(精英敌人)");
|
||
private static string EnemyTypeBossLabel => LocalizationService.Get("enemy.type.boss", "(Boss)");
|
||
private static string EnemyTypeLegendLabel => LocalizationService.Get("enemy.type.legend", "(史诗野怪)");
|
||
private static string EnemyTypeGuardianLabel => LocalizationService.Get("enemy.type.guardian", "(远古巨龙)");
|
||
public static teamUIController Instance
|
||
{
|
||
get;
|
||
private set;
|
||
}
|
||
|
||
public bool IsAnyAllyActive()
|
||
{
|
||
return isAlly01_active || isAlly02_active || isAlly03_active || isAlly04_active || isAlly05_active;
|
||
}
|
||
|
||
public bool IsAllySlotActive(int slotIndex)
|
||
{
|
||
switch (slotIndex)
|
||
{
|
||
case 0: return isAlly01_active;
|
||
case 1: return isAlly02_active;
|
||
case 2: return isAlly03_active;
|
||
case 3: return isAlly04_active;
|
||
case 4: return isAlly05_active;
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
|
||
// Documentation text normalized.
|
||
[Header("UI Effects")]
|
||
public Material grayScaleMaterial;
|
||
public Color hurtColor = Color.red;
|
||
public Color healColor = Color.green;
|
||
public Color manaColor = Color.blue;
|
||
[Range(0f, 1f)]
|
||
public float flashStartAlpha = 1f;
|
||
[Header("Flash Timing")]
|
||
public float flashHoldTime = 0.1f;
|
||
public float flashFadeDuration = 0.2f;
|
||
private static readonly int SaturationID = Shader.PropertyToID("_Saturation");
|
||
|
||
[Header("Runtime ally configuration")]
|
||
[Tooltip("IDs for the 5 ally slots (top to bottom). These are used by PopulateAllySOsFromIds to resolve SOs into currentAllySOs.")]
|
||
public List<int> allySlotIds = new List<int> { 0, 0, 0, 0, 0 };
|
||
|
||
// Documentation text normalized.
|
||
[Header("Runtime enemy configuration")]
|
||
[Tooltip("IDs for enemies to appear in sequence. All enemies share a single GameObject and will be initialized from these SOs in order.")]
|
||
public List<int> enemySlotIds = new List<int>() { 0, 0, 0, 0, 0 };
|
||
|
||
// Documentation text normalized.
|
||
private TeamCharacterDataInfo[] currentAllySOs = new TeamCharacterDataInfo[5];
|
||
// Documentation text normalized.
|
||
private EnemyData_SO[] currentEnemySOs = new EnemyData_SO[0];
|
||
// Documentation text normalized.
|
||
public EnemyData_SO[] recognizedEnemySOs = new EnemyData_SO[0];
|
||
|
||
private int CurrentDifficultyID => BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0;
|
||
|
||
// previous active flags for detecting external changes
|
||
private bool[] prevAllyActive = new bool[5];
|
||
|
||
// Documentation text normalized.
|
||
[Header("SO folder paths")]
|
||
[Tooltip("Editor-only: project path or absolute disk path to the folder that contains SOs. Example: Assets/Resources/so/ally")]
|
||
public string editorSOFolderPath = "Assets/Resources/so/ally";
|
||
|
||
[Tooltip("Runtime: Resources subfolder path (no 'Resources/' prefix). Example: so/ally")]
|
||
public string runtimeResourcesFolderPath = "so/ally";
|
||
|
||
// Documentation text normalized.
|
||
[Tooltip("Editor-only: project path or absolute disk path to the folder that contains enemy SOs. Example: Assets/Resources/so/enemies")]
|
||
public string editorEnemySOFolderPath = "Assets/Resources/so/enemies";
|
||
|
||
[Tooltip("Runtime: Resources subfolder path (no 'Resources/' prefix) for enemy SOs. Example: so/enemies")]
|
||
public string runtimeEnemyResourcesFolderPath = "so/enemies";
|
||
|
||
// Optional per-ally object to toggle together with isAllyX_active. If null falls back to objectFather_allyXX.
|
||
[Header("Ally runtime objects (optional)")]
|
||
public GameObject ally01_object;
|
||
public GameObject ally02_object;
|
||
public GameObject ally03_object;
|
||
public GameObject ally04_object;
|
||
public GameObject ally05_object;
|
||
|
||
[Header("Victory Animation")]
|
||
[Tooltip("The object to move when all enemies are defeated")]
|
||
public GameObject victoryMoveObject;
|
||
[Tooltip("Target X position (AnchoredPosition X for UI, World X for others)")]
|
||
public float victoryTargetX;
|
||
[Tooltip("Movement curve (Time 0->1, Value 0->1 recommended)")]
|
||
public AnimationCurve victoryMoveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||
[Tooltip("Duration of the movement in seconds")]
|
||
public float victoryMoveDuration = 2f;
|
||
[Tooltip("Delay before the victory animation starts (seconds)")]
|
||
public float victoryAnimationDelay = 3f;
|
||
[Tooltip("The image to fade saturation on victory")]
|
||
public Image victorySaturationImage;
|
||
[Tooltip("Duration for the saturation fade (seconds)")]
|
||
public float victorySaturationDuration = 2f;
|
||
|
||
private Coroutine _victoryMoveCoroutine;
|
||
private Coroutine _victorySaturationCoroutine;
|
||
private Coroutine _spineFlashCoroutine;
|
||
private Coroutine _spineSpawnFadeCoroutine;
|
||
private bool _pendingSpineSpawnFadeIn;
|
||
private const float SPINE_SPAWN_FADE_DURATION = 0.5f;
|
||
|
||
[Header("Ally Statistics (Real-time)")]
|
||
[Tooltip("Total damage dealt to enemies by each of the 5 ally slots.")]
|
||
public float[] totalDamageDealt = new float[5];
|
||
[Tooltip("Total HP restored by each of the 5 ally slots.")]
|
||
public float[] totalHealProvided = new float[5];
|
||
[Tooltip("Total Mana restored by each of the 5 ally slots.")]
|
||
public float[] totalManaRestored = new float[5];
|
||
[Tooltip("Total damage taken by each of the 5 ally slots.")]
|
||
public float[] totalDamageTaken = new float[5];
|
||
|
||
/// <summary>
|
||
/// Record damage dealt by an ally slot.
|
||
/// </summary>
|
||
public void RecordDamage(int slotIndex, float amount)
|
||
{
|
||
if (slotIndex >= 0 && slotIndex < 5)
|
||
{
|
||
totalDamageDealt[slotIndex] += amount;
|
||
SkillBuilder.Instance?.NotifyAllyAttackDealt(slotIndex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Record damage taken by an ally slot.
|
||
/// </summary>
|
||
public void RecordDamageTaken(int slotIndex, float amount)
|
||
{
|
||
if (slotIndex >= 0 && slotIndex < 5)
|
||
totalDamageTaken[slotIndex] += amount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Record healing provided by an ally slot.
|
||
/// </summary>
|
||
public void RecordHeal(int slotIndex, float amount)
|
||
{
|
||
if (slotIndex >= 0 && slotIndex < 5)
|
||
totalHealProvided[slotIndex] += amount;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Record mana restored by an ally slot.
|
||
/// </summary>
|
||
public void RecordMana(int slotIndex, float amount)
|
||
{
|
||
if (slotIndex >= 0 && slotIndex < 5)
|
||
totalManaRestored[slotIndex] += amount;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
[Header("Editor folder load (Editor only)")]
|
||
[Tooltip("Deprecated: kept for compatibility. Use editorSOFolderPath or runtimeResourcesFolderPath instead.")]
|
||
public string selectedProjectFolderPath = string.Empty;
|
||
|
||
/// <summary>
|
||
/// Documentation text normalized.
|
||
/// Documentation text normalized.
|
||
/// </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 is outside the project Assets folder. Use a project path like Assets/Resources/so/ally or an absolute disk path.");
|
||
}
|
||
}
|
||
|
||
// Collect TeamCharacterDataInfo and AllyHero_SO assets under the folder (or project-wide)
|
||
List<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;
|
||
|
||
// Documentation text normalized.
|
||
if (id == 0)
|
||
{
|
||
currentAllySOs[i] = null;
|
||
switch (i)
|
||
{
|
||
case 0: isAlly01_active = true; break;
|
||
case 1: isAlly02_active = true; break;
|
||
case 2: isAlly03_active = true; break;
|
||
case 3: isAlly04_active = true; break;
|
||
case 4: isAlly05_active = true; break;
|
||
}
|
||
ToggleAllyObject(i, true);
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] slot {i + 1} id=0 -> kept active (empty display)");
|
||
continue;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
for (int j = 0; j < discovered.Count; j++)
|
||
{
|
||
var obj = discovered[j];
|
||
if (obj == null) continue;
|
||
string path = AssetDatabase.GetAssetPath(obj);
|
||
if (!string.IsNullOrEmpty(path) && path.Contains(id.ToString()))
|
||
{
|
||
// if it's already TeamCharacterDataInfo, use it
|
||
if (obj is TeamCharacterDataInfo tc)
|
||
{
|
||
found = tc;
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] (Editor) matched asset by path: {path} for id={id} slot={i + 1}");
|
||
break;
|
||
}
|
||
// if it's AllyHero_SO, map to a runtime TeamCharacterDataInfo instance
|
||
if (obj is AllyHero_SO ah)
|
||
{
|
||
var mapped = ScriptableObject.CreateInstance<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;
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] (Editor) matched AllyHero_SO by path: {path} for id={id} slot={i + 1}");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
if (found == null && globalList != null && globalList.characters != null)
|
||
{
|
||
for (int j = 0; j < globalList.characters.Count; j++)
|
||
{
|
||
var so = globalList.characters[j];
|
||
if (so == null) continue;
|
||
// Documentation text normalized.
|
||
if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString()))
|
||
{
|
||
found = so;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
currentAllySOs[i] = found;
|
||
|
||
if (found == null)
|
||
{
|
||
Debug.LogWarning($"[teamUIController] 未找到 id={id} 对应的 SO (slot {i + 1})");
|
||
// keep UI slot active even when not found
|
||
switch (i)
|
||
{
|
||
case 0: isAlly01_active = true; break;
|
||
case 1: isAlly02_active = true; break;
|
||
case 2: isAlly03_active = true; break;
|
||
case 3: isAlly04_active = true; break;
|
||
case 4: isAlly05_active = true; break;
|
||
}
|
||
ToggleAllyObject(i, true);
|
||
}
|
||
else
|
||
{
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] (Editor) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID})");
|
||
// Documentation text normalized.
|
||
switch (i)
|
||
{
|
||
case 0: isAlly01_active = true; break;
|
||
case 1: isAlly02_active = true; break;
|
||
case 2: isAlly03_active = true; break;
|
||
case 3: isAlly04_active = true; break;
|
||
case 4: isAlly05_active = true; break;
|
||
}
|
||
ToggleAllyObject(i, true);
|
||
DebugCharacterSO(found, i + 1, true);
|
||
// Also attempt to set the teammate character image from an underlying AllyHero_SO if available
|
||
try
|
||
{
|
||
// first try to find AllyHero_SO by the same id
|
||
var ahList = RuntimeResourcesCache.LoadAllAllyHeroes();
|
||
AllyHero_SO matched = null;
|
||
foreach (var a in ahList) if (a != null && a.ally_heroID == id) { matched = a; break; }
|
||
if (matched != null)
|
||
{
|
||
var sprite = matched.ally_heroProfile != null ? matched.ally_heroProfile : matched.ally_heroImage;
|
||
SetTeammateCharacterImage(i, sprite);
|
||
}
|
||
else
|
||
{
|
||
// fallback: if current TeamCharacterDataInfo has a TeamSprite, use that
|
||
var tc = found as TeamCharacterDataInfo;
|
||
if (tc != null && tc.CharacterTeamSprite != null)
|
||
SetTeammateCharacterImage(i, tc.CharacterTeamSprite);
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
#else
|
||
// Runtime: prefer runtimeResourcesFolderPath, fallback to selectedProjectFolderPath
|
||
string resourcesPath = ResolveResourcesRelativePath(!string.IsNullOrEmpty(runtimeResourcesFolderPath) ? runtimeResourcesFolderPath : selectedProjectFolderPath);
|
||
List<Object> runtimeDiscovered = GetOrBuildRuntimeAllyDiscovered(resourcesPath);
|
||
|
||
TeamCharacterList globalList = null;
|
||
if (teamSettingPanel.Instance != null)
|
||
globalList = teamSettingPanel.Instance.teamCharacterList;
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
int id = allySlotIds[i];
|
||
TeamCharacterDataInfo found = null;
|
||
|
||
// Documentation text normalized.
|
||
if (id == 0)
|
||
{
|
||
currentAllySOs[i] = null;
|
||
switch (i)
|
||
{
|
||
case 0: isAlly01_active = true; break;
|
||
case 1: isAlly02_active = true; break;
|
||
case 2: isAlly03_active = true; break;
|
||
case 3: isAlly04_active = true; break;
|
||
case 4: isAlly05_active = true; break;
|
||
}
|
||
// keep associated object active
|
||
ToggleAllyObject(i, true);
|
||
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] (Runtime) slot {i+1} id=0 -> kept active (empty display)");
|
||
continue;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
string runtimeSourceInfo = string.Empty;
|
||
for (int j = 0; j < runtimeDiscovered.Count; j++)
|
||
{
|
||
var obj = runtimeDiscovered[j];
|
||
if (obj == null) continue;
|
||
// name match first
|
||
if (!string.IsNullOrEmpty(obj.name) && obj.name.Contains(id.ToString()))
|
||
{
|
||
if (obj is TeamCharacterDataInfo tc)
|
||
{
|
||
found = tc; break;
|
||
}
|
||
if (obj is AllyHero_SO ah)
|
||
{
|
||
var mapped = ScriptableObject.CreateInstance<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;
|
||
}
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
if (found == null && globalList != null && globalList.characters != null)
|
||
{
|
||
for (int j = 0; j < globalList.characters.Count; j++)
|
||
{
|
||
var so = globalList.characters[j];
|
||
if (so == null) continue;
|
||
if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString()))
|
||
{
|
||
found = so;
|
||
break;
|
||
}
|
||
if (so.CharacterID == id)
|
||
{
|
||
found = so;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// final fallback: try loading by resource path/id if resourcesPath provided
|
||
if (found == null && !string.IsNullOrEmpty(resourcesPath))
|
||
{
|
||
try
|
||
{
|
||
var byId = Resources.Load<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 = true; break;
|
||
case 1: isAlly02_active = true; break;
|
||
case 2: isAlly03_active = true; break;
|
||
case 3: isAlly04_active = true; break;
|
||
case 4: isAlly05_active = true; break;
|
||
}
|
||
ToggleAllyObject(i, true);
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
// try to get a readable source for the matched object
|
||
if (string.IsNullOrEmpty(runtimeSourceInfo))
|
||
{
|
||
runtimeSourceInfo = found.name + " (mapped)";
|
||
}
|
||
if (VerboseLogs) Debug.Log($"[teamUIcontroller] (Runtime) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID}) Source={runtimeSourceInfo}");
|
||
switch (i)
|
||
{
|
||
case 0: isAlly01_active = true; break;
|
||
case 1: isAlly02_active = true; break;
|
||
case 2: isAlly03_active = true; break;
|
||
case 3: isAlly04_active = true; break;
|
||
case 4: isAlly05_active = true; break;
|
||
}
|
||
ToggleAllyObject(i, true);
|
||
DebugCharacterSO(found, i+1, false);
|
||
// attempt to set teammate image from underlying AllyHero_SO if possible
|
||
try
|
||
{
|
||
AllyHero_SO matched = GetCachedRuntimeAllyHero(id);
|
||
if (matched != null)
|
||
{
|
||
var sprite = matched.ally_heroProfile != null ? matched.ally_heroProfile : matched.ally_heroImage;
|
||
SetTeammateCharacterImage(i, sprite);
|
||
}
|
||
else
|
||
{
|
||
var tc = found as TeamCharacterDataInfo;
|
||
if (tc != null && tc.CharacterTeamSprite != null)
|
||
SetTeammateCharacterImage(i, tc.CharacterTeamSprite);
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
private List<Object> GetOrBuildRuntimeAllyDiscovered(string resourcesPath)
|
||
{
|
||
resourcesPath = (resourcesPath ?? string.Empty).Trim('/');
|
||
if (_runtimeAllyDiscovered != null && _runtimeAllyResourcesPath == resourcesPath)
|
||
return _runtimeAllyDiscovered;
|
||
|
||
var runtimeDiscovered = new List<Object>();
|
||
if (!string.IsNullOrEmpty(resourcesPath))
|
||
{
|
||
try
|
||
{
|
||
var arr = Resources.LoadAll(resourcesPath);
|
||
if (arr != null && arr.Length > 0)
|
||
runtimeDiscovered.AddRange(arr);
|
||
|
||
if (runtimeDiscovered.Count == 0)
|
||
{
|
||
var arrAll = Resources.LoadAll("");
|
||
if (arrAll != null && arrAll.Length > 0)
|
||
{
|
||
runtimeDiscovered.AddRange(arrAll);
|
||
Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll returned 0 in '{resourcesPath}', fell back to loading all ScriptableObjects ({arrAll.Length})");
|
||
}
|
||
}
|
||
|
||
if (VerboseLogs) Debug.Log($"[teamUIcontroller] (Runtime) Loaded {runtimeDiscovered.Count} objects from Resources/{resourcesPath}");
|
||
}
|
||
catch (System.Exception ex)
|
||
{
|
||
Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll failed for '{resourcesPath}': {ex.Message}");
|
||
var arrAll = Resources.LoadAll("");
|
||
if (arrAll != null && arrAll.Length > 0)
|
||
runtimeDiscovered.AddRange(arrAll);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var arr = Resources.LoadAll("");
|
||
if (arr != null && arr.Length > 0)
|
||
runtimeDiscovered.AddRange(arr);
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] (Runtime) Loaded {arr?.Length ?? 0} objects from Resources (project-wide)");
|
||
}
|
||
|
||
_runtimeAllyResourcesPath = resourcesPath;
|
||
_runtimeAllyDiscovered = runtimeDiscovered;
|
||
_cachedRuntimeAllyHeroes = null;
|
||
_runtimeAllyHeroById = null;
|
||
return runtimeDiscovered;
|
||
}
|
||
|
||
private AllyHero_SO GetCachedRuntimeAllyHero(int id)
|
||
{
|
||
if (id <= 0) return null;
|
||
|
||
if (_runtimeAllyHeroById != null && _runtimeAllyHeroById.TryGetValue(id, out var cached))
|
||
return cached;
|
||
|
||
if (_cachedRuntimeAllyHeroes == null)
|
||
{
|
||
_cachedRuntimeAllyHeroes = RuntimeResourcesCache.LoadAllAllyHeroes() ?? System.Array.Empty<AllyHero_SO>();
|
||
_runtimeAllyHeroById = new Dictionary<int, AllyHero_SO>();
|
||
foreach (var hero in _cachedRuntimeAllyHeroes)
|
||
{
|
||
if (hero == null) continue;
|
||
_runtimeAllyHeroById[hero.ally_heroID] = hero;
|
||
}
|
||
}
|
||
|
||
return _runtimeAllyHeroById.TryGetValue(id, out var heroFound) ? heroFound : null;
|
||
}
|
||
|
||
// ResolveResourcesRelativePath helper method is missing, assuming it exists externally or is part of a base class / extension.
|
||
// Since the user is providing the full class, I will assume it's either defined outside this snippet or is implicitly handled,
|
||
// but the code within #if UNITY_EDITOR is what matters for the file content.
|
||
// For completeness, if it's not defined, the runtime block would throw an error, but as I cannot add it, I must assume it's defined elsewhere.
|
||
// A simplified placeholder for ResolveResourcesRelativePath for compilation (assuming it strips Resources/ and leading/trailing slashes):
|
||
private string ResolveResourcesRelativePath(string path)
|
||
{
|
||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||
path = path.Replace("\\", "/");
|
||
path = path.Replace("Assets/Resources/", "");
|
||
path = path.Replace("Resources/", "");
|
||
return path.Trim('/').ToLower();
|
||
}
|
||
|
||
|
||
// Populate enemy SOs from enemySlotIds using editor/runtime paths similar to allies
|
||
public void PopulateEnemySOsFromIds()
|
||
{
|
||
if (VerboseLogs) Debug.Log($"PopulateEnemySOsFromIds called, enemySlotIds: {string.Join(",", enemySlotIds)}");
|
||
if (enemySlotIds == null || enemySlotIds.Count == 0)
|
||
{
|
||
currentEnemySOs = new EnemyData_SO[0];
|
||
// update recognized list and UI
|
||
FilterRecognizedEnemies();
|
||
UpdateEnemyListText();
|
||
return;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
string folderToUse = !string.IsNullOrEmpty(editorEnemySOFolderPath) ? editorEnemySOFolderPath : selectedProjectFolderPath;
|
||
string projectRelative = null;
|
||
if (!string.IsNullOrEmpty(folderToUse))
|
||
{
|
||
string projectPath = folderToUse.Replace("\\", "/");
|
||
if (projectPath.StartsWith(Application.dataPath)) projectRelative = "Assets" + projectPath.Substring(Application.dataPath.Length);
|
||
else if (projectPath.StartsWith("Assets/")) projectRelative = projectPath;
|
||
else Debug.LogWarning("editorEnemySOFolderPath should be inside project Assets for editor search. Falling back to project-wide search.");
|
||
}
|
||
|
||
List<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 = null;
|
||
if (_runtimeEnemyDiscovered != null && _runtimeEnemyResourcesPath == resourcesPath)
|
||
{
|
||
runtimeDiscovered = _runtimeEnemyDiscovered;
|
||
}
|
||
else
|
||
{
|
||
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);
|
||
}
|
||
_runtimeEnemyResourcesPath = resourcesPath;
|
||
_runtimeEnemyDiscovered = runtimeDiscovered;
|
||
}
|
||
|
||
List<EnemyData_SO> results = new List<EnemyData_SO>();
|
||
foreach (var id in enemySlotIds)
|
||
{
|
||
if (id == 0) { results.Add(null); continue; }
|
||
EnemyData_SO found = null;
|
||
foreach (var o in runtimeDiscovered)
|
||
{
|
||
if (o == null) continue;
|
||
if (o is EnemyData_SO e)
|
||
{
|
||
if (!string.IsNullOrEmpty(e.name) && e.name.Contains(id.ToString())) { found = e; break; }
|
||
if (!string.IsNullOrEmpty(e.name) && e.name.StartsWith(id.ToString())) { found = e; break; }
|
||
if (e.enemyID == id) { found = e; break; }
|
||
}
|
||
}
|
||
// final attempt: Resources.Load by path
|
||
if (found == null && !string.IsNullOrEmpty(resourcesPath))
|
||
{
|
||
try
|
||
{
|
||
var byId = Resources.Load<EnemyData_SO>($"{resourcesPath}/{id}");
|
||
if (byId != null) found = byId;
|
||
}
|
||
catch { }
|
||
}
|
||
results.Add(found);
|
||
}
|
||
|
||
currentEnemySOs = results.ToArray();
|
||
#endif
|
||
// build recognized list and update UI
|
||
FilterRecognizedEnemies();
|
||
UpdateEnemyListText();
|
||
|
||
// Calculate total max HP for all recognized enemies
|
||
// Note: totalMaxHP is a class member and is intended to be the fixed maximum HP for the entire encounter.
|
||
RecalculateTotalMaxHP();
|
||
|
||
if (recognizedEnemySOs.Length > 0)
|
||
{
|
||
enemyCurrentCount = 0;
|
||
SpawnNextEnemy();
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
{
|
||
var cloned = Instantiate(so);
|
||
list.Add(cloned);
|
||
}
|
||
}
|
||
}
|
||
recognizedEnemySOs = list.ToArray();
|
||
// update enemyCounterMax to reflect actual number of recognized enemies
|
||
enemyCounterMax = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0;
|
||
// clamp current index
|
||
if (enemyCurrentCount < 0) enemyCurrentCount = 0;
|
||
if (enemyCurrentCount > enemyCounterMax) enemyCurrentCount = enemyCounterMax;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
public EnemyData_SO[] GetCurrentEnemySOs()
|
||
{
|
||
return (EnemyData_SO[])currentEnemySOs.Clone();
|
||
}
|
||
|
||
// Debug helper to print key fields of a TeamCharacterDataInfo
|
||
private void DebugCharacterSO(TeamCharacterDataInfo so, int slotIndex, bool isEditor)
|
||
{
|
||
if (so == null) return;
|
||
string mode = isEditor ? "Editor" : "Runtime";
|
||
string spriteCard = so.CharacterCardSprite != null ? so.CharacterCardSprite.name : "(null)";
|
||
string spriteTeam = so.CharacterTeamSprite != null ? so.CharacterTeamSprite.name : "(null)";
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] ({mode}) Slot {slotIndex} -> CharacterID={so.CharacterID}, Name={so.CharacterName}, Skill={so.CharacterSkillName}, MaxHealth={so.CharacterMaxHealth}, CardSprite={spriteCard}, TeamSprite={spriteTeam}");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Documentation text normalized.
|
||
/// </summary>
|
||
public TeamCharacterDataInfo[] GetCurrentAllySOs()
|
||
{
|
||
return (TeamCharacterDataInfo[])currentAllySOs.Clone();
|
||
}
|
||
|
||
[Header("Inspector")]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI songNametitle;
|
||
public TextMeshProUGUI difficultyName;
|
||
public TextMeshProUGUI difficultyID;
|
||
public TextMeshProUGUI constructionName;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Text comboCounter;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI enemyCounter;
|
||
[SerializeField] private int enemyCounterMax; // Documentation text normalized.
|
||
[SerializeField] private int enemyCurrentCount; // Documentation text normalized.
|
||
private bool _allEnemiesDefeatedSkillTriggered;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI currentTotalScore;
|
||
[Header("Inspector")]
|
||
public TextMeshProUGUI allSum_pmScore;
|
||
[Header("Inspector")]
|
||
public TextMeshProUGUI allSum_idolScore;
|
||
|
||
// new: runtime enemy instance and UI sync fields
|
||
private EnemyCombatant enemyCombatantInstance;
|
||
private Coroutine enemyFadeHealthCoroutine;
|
||
private Coroutine enemyFadeManaCoroutine;
|
||
private int prevEnemyHP = -1;
|
||
private int prevEnemyMana = -1;
|
||
private SkeletonDataAsset _currentEnemySkeletonDataAsset;
|
||
private SkeletonGraphic _enemySkeletonGraphic;
|
||
private SkeletonAnimation _enemySkeletonAnimation;
|
||
private float[] _spineBoundsTemp;
|
||
private SkeletonClipping _spineBoundsClipping;
|
||
private Coroutine _enemySpineDeathCoroutine;
|
||
private const string EnemyIdleAnimationName = "idle";
|
||
private const string EnemyDieAnimationName = "die";
|
||
private string _runtimeEnemyResourcesPath;
|
||
private List<Object> _runtimeEnemyDiscovered;
|
||
private string _runtimeAllyResourcesPath;
|
||
private List<Object> _runtimeAllyDiscovered;
|
||
private AllyHero_SO[] _cachedRuntimeAllyHeroes;
|
||
private Dictionary<int, AllyHero_SO> _runtimeAllyHeroById;
|
||
|
||
// For total enemy health bar
|
||
private int totalMaxHP = 0; // Documentation text normalized.
|
||
private Coroutine totalFadeHealthCoroutine;
|
||
// Documentation text normalized.
|
||
|
||
[Header("Inspector")]
|
||
public ComboJudgeType comboJudgeType = ComboJudgeType.Perfect;
|
||
|
||
private int combo = 0;
|
||
public int CurrentCombo => combo;
|
||
|
||
public enum ComboJudgeType
|
||
{
|
||
Perfect,
|
||
Great,
|
||
Good,
|
||
// Miss
|
||
}
|
||
|
||
[Header("Inspector")]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public GameObject objectFather_ally01;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public bool isAlly01_active = true;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate01_characterImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate01_healthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate01_fadehealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate01_manaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate01_fadeManaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate01_nameText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate01_healthRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate01_current_scoreText; // rate : now score / max score
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int ally01_id;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate01_hurtRedImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate01_currentHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate01_maxHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate01_currentMana;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate01_maxMana;
|
||
[Header("Inspector")]
|
||
public TextMeshProUGUI red_pmScore_sum;
|
||
[Header("Inspector")]
|
||
public TextMeshProUGUI red_idolScore_sum;
|
||
|
||
[Header("Inspector")]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public GameObject objectFather_ally02;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public bool isAlly02_active = true;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate02_characterImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate02_healthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate02_fadehealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate02_manaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate02_fadeManaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate02_nameText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate02_healthRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate02_current_scoreText; // rate : now score / max score
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int ally02_id;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate02_hurtRedImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate02_currentHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate02_maxHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate02_currentMana;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate02_maxMana;
|
||
[Header("pmScore")]
|
||
public TextMeshProUGUI green_pmScore_sum;
|
||
[Header("idolScore")]
|
||
public TextMeshProUGUI green_idolScore_sum;
|
||
|
||
[Header("teammate03")]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public GameObject objectFather_ally03;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public bool isAlly03_active = true;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate03_characterImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate03_healthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate03_fadehealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate03_manaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate03_fadeManaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate03_nameText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate03_healthRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate03_current_scoreText; // rate : now score / max score
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int ally03_id;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate03_hurtRedImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate03_currentHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate03_maxHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate03_currentMana;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate03_maxMana;
|
||
[Header("pmScore")]
|
||
public TextMeshProUGUI yellow_pmScore_sum;
|
||
[Header("idolScore")]
|
||
public TextMeshProUGUI yellow_idolScore_sum;
|
||
|
||
[Header("teammate04")]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public GameObject objectFather_ally04;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public bool isAlly04_active = true;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate04_characterImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate04_healthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate04_fadehealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate04_manaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate04_fadeManaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate04_nameText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate04_healthRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate04_current_scoreText; // rate : now score / max score
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int ally04_id;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate04_hurtRedImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate04_currentHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate04_maxHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate04_currentMana;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate04_maxMana;
|
||
[Header("pmScore")]
|
||
public TextMeshProUGUI purple_pmScore_sum;
|
||
[Header("idolScore")]
|
||
public TextMeshProUGUI purple_idolScore_sum;
|
||
|
||
[Header("teammate05")]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public GameObject objectFather_ally05;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public bool isAlly05_active = true;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate05_characterImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate05_healthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate05_fadehealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate05_manaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate05_fadeManaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate05_nameText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate05_healthRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI teammate05_current_scoreText; // rate : now score / max score
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int ally05_id;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image teammate05_hurtRedImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate05_currentHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate05_maxHP;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate05_currentMana;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int teammate05_maxMana;
|
||
[Header("pmScore")]
|
||
public TextMeshProUGUI blue_pmScore_sum;
|
||
[Header("idolScore")]
|
||
public TextMeshProUGUI blue_idolScore_sum;
|
||
|
||
[Header("currentEnemy")]
|
||
[Tooltip("Documentation text normalized.")]
|
||
public GameObject objectFather_enemy;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public bool isEnemy_active = true;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image currentEnemy_characterImage;
|
||
[Tooltip("Shown on currentEnemy_characterImage after the last enemy is defeated. If empty, falls back to the last enemy SO sprite.")]
|
||
public Sprite allEnemiesDefeatedSprite;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image currentEnemy_healthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image currentEnemy_fadehealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image currentEnemy_manaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image currentEnemy_fademanaImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image allEnemy_totalHealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image allEnemy_totalFadehealthImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Text currentEnemy_nameText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Text currentEnemy_typeText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI currentEnemy_healthRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI totalEnemy_healthRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI currentEnemy_manaRate;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI currentEnemy_current_scoreText; // rate : now score / max score
|
||
[Tooltip("Hidden after all enemies are defeated.")]
|
||
public GameObject currentEnemy_bountyObject;
|
||
[Tooltip("Displays the current enemy bounty score.")]
|
||
public Text currentEnemy_bountyText;
|
||
[Tooltip("Documentation text normalized.")]
|
||
[SerializeField] private int enemy_id;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public Image currentEnemy_hurtRedImage;
|
||
[Tooltip("Documentation text normalized.")]
|
||
public TextMeshProUGUI enemyList_rateText;
|
||
[SerializeField] private GameObject _spineSystemObject;
|
||
[SerializeField] private GameObject _imgSystemObject;
|
||
[SerializeField] private RectTransform spine_to_put;
|
||
[SerializeField] private Image spine_to_put_referenceImage;
|
||
[SerializeField] private Color spineHurtFlashColor = new Color(1f, 0.2f, 0.2f, 1f);
|
||
[SerializeField] private float spineUniformHeight = 0f;
|
||
private Coroutine enemyBountyTextCoroutine;
|
||
private bool enemyBountyTransitionActive = false;
|
||
private int currentDisplayedEnemyBounty = 0;
|
||
public float SpineUniformHeight
|
||
{
|
||
get => spineUniformHeight;
|
||
set => spineUniformHeight = value;
|
||
}
|
||
|
||
// Start is called once before the first execution of Update after the MonoBehaviour is created
|
||
void Start()
|
||
{
|
||
// Saturation material assignment moved to PlayVictoryAnimation as requested
|
||
/*
|
||
// Initialize saturation image if assigned
|
||
if (victorySaturationImage != null && grayScaleMaterial != null)
|
||
{
|
||
// Assign the grayscale material instance to the image
|
||
victorySaturationImage.material = new Material(grayScaleMaterial);
|
||
// Use _Saturation property ID
|
||
victorySaturationImage.material.SetFloat(SaturationID, 1f);
|
||
}
|
||
*/
|
||
|
||
// Load ally slot IDs from PlayerPrefs
|
||
allySlotIds[0] = PlayerPrefs.GetInt("selected_heroSlot01_heroID", 0);
|
||
allySlotIds[1] = PlayerPrefs.GetInt("selected_heroSlot02_heroID", 0);
|
||
allySlotIds[2] = PlayerPrefs.GetInt("selected_heroSlot03_heroID", 0);
|
||
allySlotIds[3] = PlayerPrefs.GetInt("selected_heroSlot04_heroID", 0);
|
||
allySlotIds[4] = PlayerPrefs.GetInt("selected_heroSlot05_heroID", 0);
|
||
|
||
PopulateAllySOsFromIds();
|
||
|
||
// initialize previous flags so Update can detect changes
|
||
prevAllyActive[0] = isAlly01_active;
|
||
prevAllyActive[1] = isAlly02_active;
|
||
prevAllyActive[2] = isAlly03_active;
|
||
prevAllyActive[3] = isAlly04_active;
|
||
prevAllyActive[4] = isAlly05_active;
|
||
|
||
// Ensure score text references are resolved so ScoreManager and AllyCombatant can write to them
|
||
ResolveScoreTextReferences();
|
||
|
||
// Initialize enemies list and spawn first enemy if any
|
||
PopulateEnemySOsFromIds();
|
||
// set enemyCounterMax based on recognized (resolved non-null) enemies so zeros/unresolved don't count
|
||
enemyCounterMax = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0;
|
||
enemyCurrentCount = 0;
|
||
InitializeEnemyInstance();
|
||
if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0)
|
||
{
|
||
SpawnNextEnemy();
|
||
}
|
||
}
|
||
|
||
// Update is called once per frame
|
||
private float _lastEnemyCheckTime = 0f;
|
||
private const float ENEMY_CHECK_INTERVAL = 0.1f; // 100ms interval for UI sync
|
||
|
||
void Update()
|
||
{
|
||
// detect external changes to isAllyX_active and toggle objects accordingly
|
||
if (prevAllyActive[0] != isAlly01_active) { ToggleAllyObject(0, isAlly01_active); prevAllyActive[0] = isAlly01_active; }
|
||
if (prevAllyActive[1] != isAlly02_active) { ToggleAllyObject(1, isAlly02_active); prevAllyActive[1] = isAlly02_active; }
|
||
if (prevAllyActive[2] != isAlly03_active) { ToggleAllyObject(2, isAlly03_active); prevAllyActive[2] = isAlly03_active; }
|
||
if (prevAllyActive[3] != isAlly04_active) { ToggleAllyObject(3, isAlly04_active); prevAllyActive[3] = isAlly04_active; }
|
||
if (prevAllyActive[4] != isAlly05_active) { ToggleAllyObject(4, isAlly05_active); prevAllyActive[4] = isAlly05_active; }
|
||
|
||
// Sync enemy UI with EnemyCombatant values - optimized to not check every single frame if possible
|
||
if (enemyCombatantInstance != null && Time.time >= _lastEnemyCheckTime + ENEMY_CHECK_INTERVAL)
|
||
{
|
||
_lastEnemyCheckTime = Time.time;
|
||
int hp = enemyCombatantInstance.currentHP;
|
||
int mana = enemyCombatantInstance.currentMana;
|
||
if (prevEnemyHP != hp)
|
||
{
|
||
UpdateEnemyHealthVisuals(prevEnemyHP, hp, true);
|
||
// Documentation text normalized.
|
||
UpdateAllEnemyTotalHealthUIOnHpChange(hp);
|
||
prevEnemyHP = hp;
|
||
}
|
||
if (prevEnemyMana != mana)
|
||
{
|
||
UpdateEnemyManaVisuals(prevEnemyMana, mana, true);
|
||
prevEnemyMana = mana;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void FixedUpdate()
|
||
{
|
||
|
||
}
|
||
|
||
private void Awake()
|
||
{
|
||
if (Instance == null)
|
||
Instance = this;
|
||
else
|
||
Destroy(gameObject);
|
||
|
||
EnsureSpineSystemObject();
|
||
EnsureImgSystemObject();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Documentation text normalized.
|
||
/// </summary>
|
||
public void OnJudgeResult(string result)
|
||
{
|
||
if (IsCombo(result))
|
||
{
|
||
combo++;
|
||
}
|
||
else
|
||
{
|
||
combo = 0;
|
||
}
|
||
if (comboCounter != null)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(combo);
|
||
comboCounter.text = _sb.ToString();
|
||
}
|
||
|
||
// --- Statistics: Update achievement tracking for combo ---
|
||
if (InGamePerformanceManager.Instance != null)
|
||
{
|
||
InGamePerformanceManager.Instance.UpdateCombo(combo);
|
||
}
|
||
else
|
||
{
|
||
// Only log once to avoid spamming every frame, or only on combo milestones
|
||
if (combo > 0 && combo % 10 == 0)
|
||
{
|
||
Debug.LogWarning("[teamUIController] InGamePerformanceManager.Instance is null! Cannot update achievements.");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Documentation text normalized.
|
||
/// </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 = SceneObjectLookupCache.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();
|
||
bool checkPm = lowHint.Contains("pm");
|
||
foreach (var t in all)
|
||
{
|
||
if (t == null || t.gameObject == null) continue;
|
||
var lowerName = t.gameObject.name.ToLower();
|
||
if (lowerName.Contains(lowHint) || (checkPm && lowerName.Contains("pm")))
|
||
return t;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// Helper: try resolve TMP under parent, then scene by name
|
||
void TryResolveScore(ref TextMeshProUGUI field, GameObject parent, string sceneName)
|
||
{
|
||
if (field != null) return;
|
||
// 1) under parent
|
||
field = FindOrConvertTMP(parent, null);
|
||
if (field != null)
|
||
{
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] Resolved score TMP from parent {parent?.name} -> {field.gameObject.name}");
|
||
return;
|
||
}
|
||
// 2) by scene object name
|
||
field = FindOrConvertTMP(null, sceneName);
|
||
if (field != null)
|
||
{
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] Resolved score TMP by scene name '{sceneName}' -> {field.gameObject.name}");
|
||
return;
|
||
}
|
||
Debug.LogWarning($"[teamUIController] Could not resolve {sceneName} TMP (parent '{parent?.name}')");
|
||
}
|
||
|
||
// Resolve per-track current score TMPs (these are usually set in inspector; fallback to parent lookup)
|
||
TryResolveScore(ref teammate01_current_scoreText, objectFather_ally01, "teammate01_current_scoreText");
|
||
TryResolveScore(ref teammate02_current_scoreText, objectFather_ally02, "teammate02_current_scoreText");
|
||
TryResolveScore(ref teammate03_current_scoreText, objectFather_ally03, "teammate03_current_scoreText");
|
||
TryResolveScore(ref teammate04_current_scoreText, objectFather_ally04, "teammate04_current_scoreText");
|
||
TryResolveScore(ref teammate05_current_scoreText, objectFather_ally05, "teammate05_current_scoreText");
|
||
|
||
// Resolve pm score TMPs and aggregate sum (recommend these fields be TextMeshProUGUI)
|
||
TryResolveScore(ref red_pmScore_sum, null, "red_pmScore_sum");
|
||
TryResolveScore(ref green_pmScore_sum, null, "green_pmScore_sum");
|
||
TryResolveScore(ref yellow_pmScore_sum, null, "yellow_pmScore_sum");
|
||
TryResolveScore(ref purple_pmScore_sum, null, "purple_pmScore_sum");
|
||
TryResolveScore(ref blue_pmScore_sum, null, "blue_pmScore_sum");
|
||
// allSum_pmScore may have been declared as TextMeshProUGUI; if it's legacy Text, convert similarly
|
||
// If you declared allSum_pmScore as TextMeshProUGUI field, do:
|
||
TryResolveScore(ref allSum_pmScore, null, "allSum_pmScore");
|
||
|
||
// total score: if assigned field is null, try to find TMP child under this object or by name "currentTotalScore"
|
||
if (currentTotalScore == null)
|
||
{
|
||
var tmp = GetComponentInChildren<TextMeshProUGUI>(true);
|
||
if (tmp != null)
|
||
{
|
||
currentTotalScore = tmp;
|
||
Debug.Log($"[teamUIController] Resolved currentTotalScore from child {tmp.gameObject.name}");
|
||
}
|
||
else
|
||
{
|
||
var byName = SceneObjectLookupCache.Find("currentTotalScore");
|
||
if (byName != null)
|
||
{
|
||
var t = byName.GetComponent<TextMeshProUGUI>() ?? byName.GetComponent<Text>()?.gameObject.AddComponent<TextMeshProUGUI>();
|
||
if (t != null) currentTotalScore = t;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Documentation text normalized.
|
||
/// </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>
|
||
/// Documentation text normalized.
|
||
/// </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;
|
||
}
|
||
}
|
||
|
||
private void SetAllyObjectBySlot(int slotIndex, GameObject value)
|
||
{
|
||
switch (slotIndex)
|
||
{
|
||
case 0: ally01_object = value; break;
|
||
case 1: ally02_object = value; break;
|
||
case 2: ally03_object = value; break;
|
||
case 3: ally04_object = value; break;
|
||
case 4: ally05_object = value; break;
|
||
}
|
||
}
|
||
|
||
private void RefreshTeammateCharacterImageBySlotId(int slotIndex)
|
||
{
|
||
if (slotIndex < 0 || slotIndex >= allySlotIds.Count) return;
|
||
int id = allySlotIds[slotIndex];
|
||
if (id <= 0) return;
|
||
|
||
AllyHero_SO[] all = RuntimeResourcesCache.LoadAllAllyHeroes();
|
||
for (int i = 0; i < all.Length; i++)
|
||
{
|
||
AllyHero_SO so = all[i];
|
||
if (so == null || so.ally_heroID != id) continue;
|
||
Sprite sprite = so.ally_heroProfile != null ? so.ally_heroProfile : so.ally_heroImage;
|
||
if (sprite != null)
|
||
{
|
||
SetTeammateCharacterImage(slotIndex, sprite);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
public void SwapAllyWithLowerAdjacent(int slotIndex)
|
||
{
|
||
int lowerSlot = slotIndex + 1;
|
||
if (slotIndex < 0 || lowerSlot > 4) return;
|
||
|
||
GameObject first = GetAllyObjectBySlot(slotIndex);
|
||
GameObject second = GetAllyObjectBySlot(lowerSlot);
|
||
int firstId = slotIndex < allySlotIds.Count ? allySlotIds[slotIndex] : 0;
|
||
int secondId = lowerSlot < allySlotIds.Count ? allySlotIds[lowerSlot] : 0;
|
||
|
||
SetAllyObjectBySlot(slotIndex, second);
|
||
SetAllyObjectBySlot(lowerSlot, first);
|
||
|
||
if (slotIndex < allySlotIds.Count) allySlotIds[slotIndex] = secondId;
|
||
if (lowerSlot < allySlotIds.Count) allySlotIds[lowerSlot] = firstId;
|
||
|
||
if (second != null)
|
||
{
|
||
AllyCombatant secondAlly = second.GetComponent<AllyCombatant>();
|
||
if (secondAlly != null) secondAlly.slotIndex = slotIndex;
|
||
}
|
||
|
||
if (first != null)
|
||
{
|
||
AllyCombatant firstAlly = first.GetComponent<AllyCombatant>();
|
||
if (firstAlly != null) firstAlly.slotIndex = lowerSlot;
|
||
}
|
||
|
||
RefreshTeammateCharacterImageBySlotId(slotIndex);
|
||
RefreshTeammateCharacterImageBySlotId(lowerSlot);
|
||
}
|
||
|
||
// ------------------- Enemy spawn & UI sync helpers -------------------
|
||
private void InitializeEnemyInstance()
|
||
{
|
||
// Try to find an existing enemy object in scene
|
||
enemyCombatantInstance = SceneObjectLookupCache.FindFirst<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 EnsureSpineSystemObject()
|
||
{
|
||
if (spine_to_put != null)
|
||
{
|
||
_spineSystemObject = spine_to_put.gameObject;
|
||
}
|
||
if (_spineSystemObject == null)
|
||
{
|
||
Transform searchRoot = objectFather_enemy != null ? objectFather_enemy.transform : transform;
|
||
var existing = searchRoot.Find("spinesystemobject");
|
||
if (existing != null)
|
||
{
|
||
_spineSystemObject = existing.gameObject;
|
||
}
|
||
}
|
||
if (_spineSystemObject == null)
|
||
{
|
||
_spineSystemObject = new GameObject("spinesystemobject", typeof(RectTransform));
|
||
Transform targetParent = objectFather_enemy != null ? objectFather_enemy.transform : (currentEnemy_characterImage != null ? currentEnemy_characterImage.transform.parent : transform);
|
||
_spineSystemObject.transform.SetParent(targetParent, false);
|
||
}
|
||
if (_enemySkeletonGraphic == null)
|
||
{
|
||
_enemySkeletonGraphic = _spineSystemObject.GetComponent<SkeletonGraphic>();
|
||
if (_enemySkeletonGraphic == null) _enemySkeletonGraphic = _spineSystemObject.AddComponent<SkeletonGraphic>();
|
||
}
|
||
if (_enemySkeletonAnimation == null || _enemySkeletonAnimation.gameObject != _spineSystemObject)
|
||
{
|
||
_enemySkeletonAnimation = _spineSystemObject.GetComponent<SkeletonAnimation>();
|
||
if (_enemySkeletonAnimation == null) _enemySkeletonAnimation = _spineSystemObject.AddComponent<SkeletonAnimation>();
|
||
}
|
||
}
|
||
|
||
private void EnsureImgSystemObject()
|
||
{
|
||
if (_imgSystemObject == null)
|
||
{
|
||
if (currentEnemy_characterImage != null)
|
||
{
|
||
_imgSystemObject = currentEnemy_characterImage.gameObject;
|
||
}
|
||
else
|
||
{
|
||
Transform searchRoot = objectFather_enemy != null ? objectFather_enemy.transform : transform;
|
||
var existing = searchRoot.Find("imgSys");
|
||
if (existing != null)
|
||
{
|
||
_imgSystemObject = existing.gameObject;
|
||
}
|
||
}
|
||
}
|
||
if (_imgSystemObject == null)
|
||
{
|
||
_imgSystemObject = new GameObject("imgSys", typeof(RectTransform));
|
||
Transform targetParent = objectFather_enemy != null ? objectFather_enemy.transform : transform;
|
||
_imgSystemObject.transform.SetParent(targetParent, false);
|
||
}
|
||
}
|
||
|
||
private void UpdateSpineRectForHeight(EnemyData_SO so)
|
||
{
|
||
if (_enemySkeletonGraphic == null) return;
|
||
if (so == null) return;
|
||
var rect = _enemySkeletonGraphic.rectTransform;
|
||
if (rect == null) return;
|
||
float targetHeight = spineUniformHeight;
|
||
if (targetHeight <= 0f && spine_to_put_referenceImage != null) targetHeight = spine_to_put_referenceImage.rectTransform.rect.height;
|
||
else if (targetHeight <= 0f && currentEnemy_characterImage != null) targetHeight = currentEnemy_characterImage.rectTransform.rect.height;
|
||
if (targetHeight <= 0f) return;
|
||
var skeleton = _enemySkeletonGraphic.Skeleton;
|
||
if (skeleton == null) return;
|
||
if (_spineBoundsClipping == null) _spineBoundsClipping = new SkeletonClipping();
|
||
float minX;
|
||
float minY;
|
||
float width;
|
||
float height;
|
||
skeleton.GetBounds(out minX, out minY, out width, out height, ref _spineBoundsTemp, _spineBoundsClipping);
|
||
float skeletonHeight = height;
|
||
float skeletonWidth = width;
|
||
if (skeletonHeight <= 0f || skeletonWidth <= 0f) return;
|
||
float targetWidth = skeletonWidth * (targetHeight / skeletonHeight);
|
||
float ratio = so.ScaleRatio;
|
||
if (ratio <= 0f) ratio = 1f;
|
||
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rect.pivot = new Vector2(0.5f, 0.5f);
|
||
rect.sizeDelta = new Vector2(targetWidth, targetHeight);
|
||
rect.anchoredPosition = Vector2.zero;
|
||
if (spine_to_put != null)
|
||
{
|
||
float xScale = so.FlipX ? -ratio : ratio;
|
||
spine_to_put.localScale = new Vector3(xScale, ratio, 1f);
|
||
spine_to_put.anchoredPosition = new Vector2(so.XOffset, so.YOffset);
|
||
}
|
||
else
|
||
{
|
||
rect.sizeDelta = new Vector2(targetWidth * ratio, targetHeight * ratio);
|
||
rect.anchoredPosition = new Vector2(so.XOffset, so.YOffset);
|
||
rect.localScale = new Vector3(so.FlipX ? -1f : 1f, 1f, 1f);
|
||
}
|
||
}
|
||
|
||
private TrackEntry TryPlayEnemySpineAnimation(string animationName, bool loop)
|
||
{
|
||
if (_enemySkeletonGraphic == null) return null;
|
||
var skeleton = _enemySkeletonGraphic.Skeleton;
|
||
if (skeleton == null) return null;
|
||
if (skeleton.Data.FindAnimation(animationName) == null) return null;
|
||
if (_enemySkeletonAnimation == null) return null;
|
||
var state = _enemySkeletonAnimation.AnimationState;
|
||
if (state == null) return null;
|
||
return state.SetAnimation(0, animationName, loop);
|
||
}
|
||
|
||
private IEnumerator PlayEnemyDieThenAdvance(float delay)
|
||
{
|
||
yield return new WaitForSeconds(delay);
|
||
_enemySpineDeathCoroutine = null;
|
||
|
||
// Check if this was the last enemy and it was using Spine
|
||
if (enemyCurrentCount == recognizedEnemySOs.Length - 1)
|
||
{
|
||
var so = recognizedEnemySOs[enemyCurrentCount];
|
||
if (so != null && so.skeletonDataAsset != null)
|
||
{
|
||
// Start transition to static image for the last Spine enemy
|
||
if (gameObject.activeInHierarchy)
|
||
StartCoroutine(TransitionSpineToStaticImage(so));
|
||
yield break; // Do not increment enemyCurrentCount or call SpawnNextEnemy yet
|
||
}
|
||
}
|
||
|
||
enemyCurrentCount++;
|
||
SpawnNextEnemy();
|
||
}
|
||
|
||
private IEnumerator TransitionSpineToStaticImage(EnemyData_SO so)
|
||
{
|
||
// 1. Ensure Img System Object is ready
|
||
EnsureImgSystemObject();
|
||
if (_imgSystemObject != null) _imgSystemObject.SetActive(true);
|
||
|
||
// Stop any pending hurt flash
|
||
StopCoroutine("EnemyHurtFlash");
|
||
if (_spineFlashCoroutine != null)
|
||
{
|
||
StopCoroutine(_spineFlashCoroutine);
|
||
_spineFlashCoroutine = null;
|
||
}
|
||
|
||
// 2. Setup the static image
|
||
if (currentEnemy_hurtRedImage != null)
|
||
{
|
||
Color hurtColor = currentEnemy_hurtRedImage.color;
|
||
hurtColor.a = 0f;
|
||
currentEnemy_hurtRedImage.color = hurtColor;
|
||
}
|
||
if (currentEnemy_characterImage != null)
|
||
{
|
||
// Use the configured victory sprite first, otherwise fall back to the defeated enemy sprite.
|
||
var sprite = allEnemiesDefeatedSprite != null
|
||
? allEnemiesDefeatedSprite
|
||
: (so.enemy_Image != null ? so.enemy_Image : (so.enemy_Profile != null ? so.enemy_Profile : null));
|
||
currentEnemy_characterImage.sprite = sprite;
|
||
|
||
// Apply grayscale material
|
||
if (grayScaleMaterial != null)
|
||
{
|
||
currentEnemy_characterImage.material = new Material(grayScaleMaterial);
|
||
currentEnemy_characterImage.material.SetFloat(SaturationID, 0f); // 0 means grayscale
|
||
}
|
||
|
||
// Start with alpha 0 for fade in
|
||
Color c = currentEnemy_characterImage.color;
|
||
c.a = 0f;
|
||
currentEnemy_characterImage.color = c;
|
||
|
||
// 3. Fade in over 1 second
|
||
float timer = 0f;
|
||
float duration = 1.0f;
|
||
while (timer < duration)
|
||
{
|
||
timer += Time.deltaTime;
|
||
float alpha = Mathf.Clamp01(timer / duration);
|
||
c.a = alpha;
|
||
currentEnemy_characterImage.color = c;
|
||
yield return null;
|
||
}
|
||
|
||
// Ensure fully opaque at the end
|
||
c.a = 1f;
|
||
currentEnemy_characterImage.color = c;
|
||
}
|
||
|
||
// 4. Finally advance to the "All Defeated" state
|
||
if (currentEnemy_hurtRedImage != null)
|
||
{
|
||
Color hurtColor = currentEnemy_hurtRedImage.color;
|
||
hurtColor.a = 0f;
|
||
currentEnemy_hurtRedImage.color = hurtColor;
|
||
}
|
||
enemyCurrentCount++;
|
||
SpawnNextEnemy();
|
||
}
|
||
|
||
private void ApplyEnemyVisuals(EnemyData_SO so)
|
||
{
|
||
if (so == null)
|
||
{
|
||
_currentEnemySkeletonDataAsset = null;
|
||
if (_spineSystemObject != null) _spineSystemObject.SetActive(false);
|
||
if (_imgSystemObject != null) _imgSystemObject.SetActive(false);
|
||
if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
|
||
return;
|
||
}
|
||
|
||
if (so.skeletonDataAsset != null)
|
||
{
|
||
EnsureSpineSystemObject();
|
||
EnsureImgSystemObject();
|
||
if (_currentEnemySkeletonDataAsset != so.skeletonDataAsset)
|
||
{
|
||
_currentEnemySkeletonDataAsset = so.skeletonDataAsset;
|
||
if (_enemySkeletonAnimation != null)
|
||
{
|
||
_enemySkeletonAnimation.skeletonDataAsset = _currentEnemySkeletonDataAsset;
|
||
_enemySkeletonAnimation.Initialize(true);
|
||
}
|
||
if (_enemySkeletonGraphic != null)
|
||
{
|
||
_enemySkeletonGraphic.skeletonDataAsset = _currentEnemySkeletonDataAsset;
|
||
_enemySkeletonGraphic.Initialize(true);
|
||
}
|
||
}
|
||
if (_enemySkeletonGraphic != null)
|
||
{
|
||
UpdateSpineRectForHeight(so);
|
||
TryPlayEnemySpineAnimation(EnemyIdleAnimationName, true);
|
||
}
|
||
if (_spineSystemObject != null) _spineSystemObject.SetActive(true);
|
||
if (_imgSystemObject != null) _imgSystemObject.SetActive(false);
|
||
if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
|
||
return;
|
||
}
|
||
|
||
_currentEnemySkeletonDataAsset = null;
|
||
if (_spineSystemObject != null) _spineSystemObject.SetActive(false);
|
||
EnsureImgSystemObject();
|
||
if (_imgSystemObject != null) _imgSystemObject.SetActive(true);
|
||
if (currentEnemy_characterImage != null)
|
||
{
|
||
var sprite = so.enemy_Profile != null ? so.enemy_Profile : so.enemy_Image;
|
||
if (sprite != null) currentEnemy_characterImage.sprite = sprite;
|
||
else currentEnemy_characterImage.sprite = null;
|
||
}
|
||
}
|
||
|
||
private void SpawnNextEnemy()
|
||
{
|
||
if (_enemySpineDeathCoroutine != null)
|
||
{
|
||
StopCoroutine(_enemySpineDeathCoroutine);
|
||
_enemySpineDeathCoroutine = null;
|
||
}
|
||
// handle case: no configured/recognized enemies -> show empty state but keep UI visible
|
||
if (recognizedEnemySOs == null || recognizedEnemySOs.Length == 0)
|
||
{
|
||
_currentEnemySkeletonDataAsset = null;
|
||
if (_spineSystemObject != null) _spineSystemObject.SetActive(false);
|
||
if (_imgSystemObject != null) _imgSystemObject.SetActive(false);
|
||
if (objectFather_enemy != null) objectFather_enemy.SetActive(true);
|
||
if (currentEnemy_nameText != null) currentEnemy_nameText.text = NoEnemyLabel;
|
||
if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
|
||
if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = 0f;
|
||
if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = 0f;
|
||
if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = "0/0";
|
||
if (currentEnemy_manaImage != null) currentEnemy_manaImage.fillAmount = 0f;
|
||
if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = 0f;
|
||
if (currentEnemy_typeText != null) currentEnemy_typeText.text = EnemyTypeNormalLabel;
|
||
HideEnemyBountyUiImmediate();
|
||
UpdateEnemyListText();
|
||
// Documentation text normalized.
|
||
UpdateAllEnemyTotalHealthUIImmediate();
|
||
return;
|
||
}
|
||
|
||
if (enemyCurrentCount < 0) enemyCurrentCount = 0;
|
||
if (enemyCurrentCount == 0) _allEnemiesDefeatedSkillTriggered = false;
|
||
if (enemyCurrentCount >= recognizedEnemySOs.Length)
|
||
{
|
||
// All enemies processed: show empty/finished state but keep UI visible
|
||
_currentEnemySkeletonDataAsset = null;
|
||
if (_spineSystemObject != null) _spineSystemObject.SetActive(false);
|
||
|
||
// Fix: Keep _imgSystemObject active if we just transitioned to a static image
|
||
if (_imgSystemObject != null && !(_imgSystemObject.activeSelf && currentEnemy_characterImage != null && currentEnemy_characterImage.sprite != null))
|
||
{
|
||
_imgSystemObject.SetActive(false);
|
||
}
|
||
else if (_imgSystemObject != null)
|
||
{
|
||
// Ensure it stays active
|
||
_imgSystemObject.SetActive(true);
|
||
}
|
||
|
||
if (objectFather_enemy != null) objectFather_enemy.SetActive(true);
|
||
if (currentEnemy_hurtRedImage != null)
|
||
{
|
||
Color hurtColor = currentEnemy_hurtRedImage.color;
|
||
hurtColor.a = 0f;
|
||
currentEnemy_hurtRedImage.color = hurtColor;
|
||
}
|
||
if (currentEnemy_nameText != null) currentEnemy_nameText.text = EnemyDefeatedLabel;
|
||
// if (currentEnemy_characterImage != null) currentEnemy_characterImage.sprite = null;
|
||
if (currentEnemy_healthImage != null) currentEnemy_healthImage.fillAmount = 0f;
|
||
if (currentEnemy_fadehealthImage != null) currentEnemy_fadehealthImage.fillAmount = 0f;
|
||
if (currentEnemy_healthRate != null) currentEnemy_healthRate.text = "0/0";
|
||
if (currentEnemy_manaImage != null) currentEnemy_manaImage.fillAmount = 0f;
|
||
if (currentEnemy_fademanaImage != null) currentEnemy_fademanaImage.fillAmount = 0f;
|
||
if (currentEnemy_typeText != null) currentEnemy_typeText.text = EnemyTypeNormalLabel;
|
||
if (!enemyBountyTransitionActive)
|
||
HideEnemyBountyUiImmediate();
|
||
UpdateEnemyListText();
|
||
// Update total health bar to 0
|
||
UpdateAllEnemyTotalHealthUIImmediate();
|
||
|
||
// Trigger victory animation
|
||
PlayVictoryAnimation();
|
||
|
||
// Fire "all enemies defeated" skill trigger exactly once for this run.
|
||
if (!_allEnemiesDefeatedSkillTriggered && Application.isPlaying)
|
||
{
|
||
_allEnemiesDefeatedSkillTriggered = true;
|
||
if (SkillBuilder.Instance != null)
|
||
{
|
||
SkillBuilder.Instance.TriggerOnAllEnemiesDefeated();
|
||
if (VerboseLogs) Debug.Log("[teamUIController] All enemies defeated -> TriggerOnAllEnemiesDefeated fired.");
|
||
}
|
||
else
|
||
{
|
||
var sb = SceneObjectLookupCache.FindAny<SkillBuilder>();
|
||
if (sb != null)
|
||
{
|
||
sb.TriggerOnAllEnemiesDefeated();
|
||
Debug.LogWarning("[teamUIController] SkillBuilder.Instance was null when firing AllEnemiesDefeated; used FindObjectOfType fallback");
|
||
}
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
var so = recognizedEnemySOs[enemyCurrentCount];
|
||
if (so == null)
|
||
{
|
||
Debug.LogWarning($"[teamUIController] SpawnNextEnemy: SO is null at index {enemyCurrentCount}");
|
||
// skip null slots (treated as empty) and advance
|
||
enemyCurrentCount++;
|
||
SpawnNextEnemy();
|
||
return;
|
||
}
|
||
|
||
InitializeEnemyInstance();
|
||
int difficultyID = BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0;
|
||
enemyCombatantInstance.InitializeFromSO(so, difficultyID);
|
||
|
||
// set UI visuals (name / sprites)
|
||
string enemyName = so.enemyName ?? so.name;
|
||
if (currentEnemy_nameText != null) currentEnemy_nameText.text = enemyName;
|
||
ApplyEnemyVisuals(so);
|
||
TryStartEnemySpineSpawnFade(so);
|
||
|
||
// Push spawn message to feed
|
||
SkillTriggerFeedUI.PushEnemySpawn(enemyName);
|
||
|
||
// set type text: show for Simple/Elite/Boss/Legend
|
||
if (currentEnemy_typeText != null)
|
||
{
|
||
switch (so.enemyType)
|
||
{
|
||
case EnemyData_SO.EnemyType.Elite:
|
||
currentEnemy_typeText.text = EnemyTypeEliteLabel;
|
||
break;
|
||
case EnemyData_SO.EnemyType.Boss:
|
||
currentEnemy_typeText.text = EnemyTypeBossLabel;
|
||
break;
|
||
case EnemyData_SO.EnemyType.Legend:
|
||
currentEnemy_typeText.text = EnemyTypeLegendLabel;
|
||
break;
|
||
case EnemyData_SO.EnemyType.Guardian:
|
||
currentEnemy_typeText.text = EnemyTypeGuardianLabel;
|
||
break;
|
||
default:
|
||
currentEnemy_typeText.text = EnemyTypeNormalLabel;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ensure UI parent is visible
|
||
if (objectFather_enemy != null) objectFather_enemy.SetActive(true);
|
||
|
||
int currentEnemyBounty = CalculateEnemyBountyScore(so);
|
||
if (currentEnemy_bountyObject != null)
|
||
currentEnemy_bountyObject.SetActive(true);
|
||
if (enemyCurrentCount == 0 && currentDisplayedEnemyBounty == 0 && !enemyBountyTransitionActive)
|
||
{
|
||
if (enemyBountyTextCoroutine != null)
|
||
{
|
||
StopCoroutine(enemyBountyTextCoroutine);
|
||
enemyBountyTextCoroutine = null;
|
||
}
|
||
SetEnemyBountyDisplayImmediate(currentEnemyBounty);
|
||
}
|
||
else
|
||
{
|
||
AnimateEnemyBountyRise(currentEnemyBounty);
|
||
}
|
||
|
||
// reset previous values so Update picks up initial state
|
||
prevEnemyHP = enemyCombatantInstance.currentHP;
|
||
prevEnemyMana = enemyCombatantInstance.currentMana;
|
||
UpdateEnemyUIImmediate();
|
||
|
||
// update enemy counter text
|
||
if (enemyCounter != null)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(Mathf.Clamp(enemyCurrentCount + 1, 1, 999)).Append("/").Append(enemyCounterMax);
|
||
enemyCounter.text = _sb.ToString();
|
||
}
|
||
|
||
// refresh the enemy list display
|
||
UpdateEnemyListText();
|
||
}
|
||
|
||
// Build a readable representation of the enemy queue and write to enemyList_rateText
|
||
private void UpdateEnemyListText()
|
||
{
|
||
if (enemyList_rateText == null) return;
|
||
|
||
// Use actual recognized enemy count
|
||
int total = recognizedEnemySOs != null ? recognizedEnemySOs.Length : 0;
|
||
int displayIndex = 0;
|
||
|
||
if (total > 0)
|
||
{
|
||
// enemyCurrentCount is 0-based index
|
||
// ensure display is 1 to total
|
||
displayIndex = Mathf.Clamp(enemyCurrentCount + 1, 1, total);
|
||
}
|
||
|
||
_sb.Clear();
|
||
_sb.Append(displayIndex).Append("/").Append(total);
|
||
enemyList_rateText.text = _sb.ToString();
|
||
}
|
||
|
||
private void PlayVictoryAnimation()
|
||
{
|
||
// 1. Victory Movement (Delayed)
|
||
if (victoryMoveObject != null)
|
||
{
|
||
if (_victoryMoveCoroutine != null) StopCoroutine(_victoryMoveCoroutine);
|
||
if (gameObject.activeInHierarchy)
|
||
_victoryMoveCoroutine = StartCoroutine(VictoryMoveRoutine());
|
||
}
|
||
|
||
// 2. Victory Saturation Set (Immediate)
|
||
if (victorySaturationImage != null)
|
||
{
|
||
if (_victorySaturationCoroutine != null) StopCoroutine(_victorySaturationCoroutine);
|
||
|
||
// Assign material only when victory occurs
|
||
if (grayScaleMaterial != null)
|
||
{
|
||
victorySaturationImage.material = new Material(grayScaleMaterial);
|
||
}
|
||
|
||
// Immediately set saturation to 0 (Grayscale)
|
||
victorySaturationImage.material.SetFloat(SaturationID, 0f);
|
||
}
|
||
}
|
||
|
||
private IEnumerator VictoryMoveRoutine()
|
||
{
|
||
if (victoryMoveObject == null) yield break;
|
||
|
||
// Wait for the specified delay before starting movement
|
||
if (victoryAnimationDelay > 0f)
|
||
yield return new WaitForSeconds(victoryAnimationDelay);
|
||
|
||
float timer = 0f;
|
||
float duration = victoryMoveDuration > 0 ? victoryMoveDuration : 2f;
|
||
|
||
RectTransform rt = victoryMoveObject.GetComponent<RectTransform>();
|
||
Transform tf = victoryMoveObject.transform;
|
||
|
||
float startX = 0f;
|
||
bool isUI = (rt != null);
|
||
|
||
if (isUI) startX = rt.anchoredPosition.x;
|
||
else startX = tf.position.x;
|
||
|
||
// If curve is not set, default to EaseInOut
|
||
if (victoryMoveCurve == null || victoryMoveCurve.length == 0)
|
||
victoryMoveCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
|
||
|
||
while (timer < duration)
|
||
{
|
||
timer += Time.deltaTime;
|
||
float progress = Mathf.Clamp01(timer / duration);
|
||
float curveValue = victoryMoveCurve.Evaluate(progress);
|
||
|
||
float newX = Mathf.LerpUnclamped(startX, victoryTargetX, curveValue);
|
||
|
||
if (isUI)
|
||
{
|
||
Vector2 pos = rt.anchoredPosition;
|
||
pos.x = newX;
|
||
rt.anchoredPosition = pos;
|
||
}
|
||
else
|
||
{
|
||
Vector3 pos = tf.position;
|
||
pos.x = newX;
|
||
tf.position = pos;
|
||
}
|
||
|
||
yield return null;
|
||
}
|
||
|
||
// Final set
|
||
if (isUI)
|
||
{
|
||
Vector2 pos = rt.anchoredPosition;
|
||
pos.x = victoryTargetX;
|
||
rt.anchoredPosition = pos;
|
||
}
|
||
else
|
||
{
|
||
Vector3 pos = tf.position;
|
||
pos.x = victoryTargetX;
|
||
tf.position = pos;
|
||
}
|
||
}
|
||
|
||
// VictorySaturationRoutine removed as requested (immediate set)
|
||
|
||
private void OnEnemyDiedHandler(EnemyCombatant e)
|
||
{
|
||
// Push death message to feed before advancing
|
||
string eName = "Unknown Enemy";
|
||
EnemyData_SO so = null;
|
||
if (enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length)
|
||
{
|
||
so = recognizedEnemySOs[enemyCurrentCount];
|
||
if (so != null) eName = so.enemyName ?? so.name;
|
||
}
|
||
SkillTriggerFeedUI.PushEnemyDeath(eName);
|
||
|
||
// notify enemy ball UI
|
||
if (BeatmapManager.Instance != null && BeatmapManager.Instance.enemyBallLoader != null)
|
||
{
|
||
BeatmapManager.Instance.enemyBallLoader.OnEnemyDefeated();
|
||
}
|
||
|
||
if (so != null && so.skeletonDataAsset != null)
|
||
{
|
||
var entry = TryPlayEnemySpineAnimation(EnemyDieAnimationName, false);
|
||
float delay = 0f;
|
||
if (entry != null)
|
||
{
|
||
delay = entry.AnimationEnd - entry.AnimationStart;
|
||
if (delay <= 0f && entry.Animation != null) delay = entry.Animation.Duration;
|
||
}
|
||
if (delay > 0f)
|
||
{
|
||
int nextIndex = enemyCurrentCount + 1;
|
||
if (nextIndex >= recognizedEnemySOs.Length)
|
||
{
|
||
AnimateEnemyBountyToZero(true);
|
||
}
|
||
else
|
||
{
|
||
AnimateEnemyBountyToZero(false);
|
||
}
|
||
|
||
if (_enemySpineDeathCoroutine != null)
|
||
{
|
||
StopCoroutine(_enemySpineDeathCoroutine);
|
||
_enemySpineDeathCoroutine = null;
|
||
}
|
||
_pendingSpineSpawnFadeIn = true;
|
||
if (gameObject.activeInHierarchy)
|
||
_enemySpineDeathCoroutine = StartCoroutine(PlayEnemyDieThenAdvance(delay));
|
||
return;
|
||
}
|
||
}
|
||
|
||
int nextEnemyIndex = enemyCurrentCount + 1;
|
||
if (nextEnemyIndex >= recognizedEnemySOs.Length)
|
||
{
|
||
AnimateEnemyBountyToZero(true);
|
||
}
|
||
else
|
||
{
|
||
AnimateEnemyBountyToZero(false);
|
||
}
|
||
|
||
enemyCurrentCount++;
|
||
SpawnNextEnemy();
|
||
}
|
||
|
||
private void OnEnemyRevivedHandler(EnemyCombatant e)
|
||
{
|
||
// refresh UI
|
||
UpdateEnemyUIImmediate();
|
||
}
|
||
|
||
private int CalculateEnemyBountyScore(EnemyData_SO so)
|
||
{
|
||
if (so == null)
|
||
return 0;
|
||
|
||
int difficultyID = BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0;
|
||
EnemyData_SO.EnemyDifficultyStats stats = so.GetStatsByDifficultyID(difficultyID);
|
||
int calculatedMaxHp = Mathf.Max(0, stats.enemy_maxHP);
|
||
float difficultyLevel = ResolveCurrentDifficultyLevel(difficultyID);
|
||
float totalBounty = calculatedMaxHp * 0.05f * difficultyLevel + so.enemyBountyBonus;
|
||
return Mathf.Max(0, Mathf.CeilToInt(totalBounty));
|
||
}
|
||
|
||
private float ResolveCurrentDifficultyLevel(int difficultyID)
|
||
{
|
||
BeatmapManager beatmapManager = BeatmapManager.Instance;
|
||
if (beatmapManager == null || beatmapManager.assignedSongData == null || beatmapManager.assignedSongData.chartFiles == null)
|
||
return 1f;
|
||
|
||
List<ChartFileEntry> chartFiles = beatmapManager.assignedSongData.chartFiles;
|
||
for (int i = 0; i < chartFiles.Count; i++)
|
||
{
|
||
ChartFileEntry entry = chartFiles[i];
|
||
if (entry == null || entry.difficulty != difficultyID)
|
||
continue;
|
||
|
||
return Mathf.Max(0f, entry.difficultyLEVEL);
|
||
}
|
||
|
||
return 1f;
|
||
}
|
||
|
||
private void SetEnemyBountyDisplayImmediate(int value)
|
||
{
|
||
currentDisplayedEnemyBounty = Mathf.Max(0, value);
|
||
if (currentEnemy_bountyText != null)
|
||
currentEnemy_bountyText.text = currentDisplayedEnemyBounty.ToString();
|
||
}
|
||
|
||
private void HideEnemyBountyUiImmediate()
|
||
{
|
||
if (enemyBountyTextCoroutine != null)
|
||
{
|
||
StopCoroutine(enemyBountyTextCoroutine);
|
||
enemyBountyTextCoroutine = null;
|
||
}
|
||
|
||
enemyBountyTransitionActive = false;
|
||
SetEnemyBountyDisplayImmediate(0);
|
||
if (currentEnemy_bountyObject != null)
|
||
currentEnemy_bountyObject.SetActive(false);
|
||
}
|
||
|
||
private void AnimateEnemyBountyRise(int targetValue)
|
||
{
|
||
if (enemyBountyTextCoroutine != null)
|
||
StopCoroutine(enemyBountyTextCoroutine);
|
||
|
||
if (currentEnemy_bountyObject != null)
|
||
currentEnemy_bountyObject.SetActive(true);
|
||
|
||
enemyBountyTransitionActive = true;
|
||
enemyBountyTextCoroutine = StartCoroutine(AnimateEnemyBountyValueRoutine(0, Mathf.Max(0, targetValue), 1f, false));
|
||
}
|
||
|
||
private void AnimateEnemyBountyToZero(bool hideAfterZero)
|
||
{
|
||
if (enemyBountyTextCoroutine != null)
|
||
StopCoroutine(enemyBountyTextCoroutine);
|
||
|
||
if (currentEnemy_bountyObject != null)
|
||
currentEnemy_bountyObject.SetActive(true);
|
||
|
||
enemyBountyTransitionActive = true;
|
||
enemyBountyTextCoroutine = StartCoroutine(AnimateEnemyBountyValueRoutine(currentDisplayedEnemyBounty, 0, 1f, hideAfterZero));
|
||
}
|
||
|
||
private IEnumerator AnimateEnemyBountyValueRoutine(int startValue, int endValue, float duration, bool deactivateAtEnd)
|
||
{
|
||
enemyBountyTransitionActive = true;
|
||
SetEnemyBountyDisplayImmediate(startValue);
|
||
|
||
if (duration <= 0f)
|
||
{
|
||
SetEnemyBountyDisplayImmediate(endValue);
|
||
}
|
||
else
|
||
{
|
||
float elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
elapsed += Time.deltaTime;
|
||
float t = Mathf.Clamp01(elapsed / duration);
|
||
int value = Mathf.RoundToInt(Mathf.Lerp(startValue, endValue, t));
|
||
SetEnemyBountyDisplayImmediate(value);
|
||
yield return null;
|
||
}
|
||
SetEnemyBountyDisplayImmediate(endValue);
|
||
}
|
||
|
||
enemyBountyTransitionActive = false;
|
||
enemyBountyTextCoroutine = null;
|
||
|
||
if (deactivateAtEnd && currentEnemy_bountyObject != null)
|
||
currentEnemy_bountyObject.SetActive(false);
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
|
||
// =========================================================
|
||
// Documentation text normalized.
|
||
// =========================================================
|
||
/// <summary>
|
||
/// Documentation text normalized.
|
||
/// </summary>
|
||
public void ApplyCalculatedEnemyHP(int individualMaxHP)
|
||
{
|
||
List<int> hpList = new List<int>();
|
||
if (recognizedEnemySOs != null)
|
||
{
|
||
for (int i = 0; i < recognizedEnemySOs.Length; i++)
|
||
{
|
||
hpList.Add(individualMaxHP);
|
||
}
|
||
}
|
||
ApplyCalculatedEnemyHP(hpList);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Apply individual HP values to enemies.
|
||
/// </summary>
|
||
public void ApplyCalculatedEnemyHP(List<int> individualHPList)
|
||
{
|
||
if (recognizedEnemySOs != null && recognizedEnemySOs.Length > 0 && individualHPList != null)
|
||
{
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] Applying calculated HP list to {recognizedEnemySOs.Length} recognized enemies.");
|
||
|
||
for (int i = 0; i < recognizedEnemySOs.Length; i++)
|
||
{
|
||
var so = recognizedEnemySOs[i];
|
||
if (so != null)
|
||
{
|
||
int newHP = (i < individualHPList.Count) ? individualHPList[i] : 0;
|
||
so.SetMaxHPByDifficulty(CurrentDifficultyID, newHP);
|
||
}
|
||
}
|
||
|
||
RecalculateTotalMaxHP();
|
||
|
||
if (enemyCombatantInstance != null && enemyCurrentCount >= 0 && enemyCurrentCount < recognizedEnemySOs.Length)
|
||
{
|
||
var currentSO = recognizedEnemySOs[enemyCurrentCount];
|
||
if (currentSO != null)
|
||
{
|
||
int diffID = BeatmapManager.Instance != null ? BeatmapManager.Instance.assignedDifficulty : 0;
|
||
var stats = currentSO.GetStatsByDifficultyID(diffID);
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] Re-initializing current enemy instance with new HP: {stats.enemy_maxHP}");
|
||
enemyCombatantInstance.InitializeFromSO(currentSO, diffID);
|
||
UpdateEnemyUIImmediate();
|
||
|
||
// The first enemy can be spawned before BeatmapManager finishes writing
|
||
// the calculated HP back into the enemy SO. Refresh bounty text here so it
|
||
// reflects the final computed max HP rather than the pre-calculation value.
|
||
int refreshedBounty = CalculateEnemyBountyScore(currentSO);
|
||
if (currentEnemy_bountyObject != null)
|
||
currentEnemy_bountyObject.SetActive(true);
|
||
if (!enemyBountyTransitionActive)
|
||
SetEnemyBountyDisplayImmediate(refreshedBounty);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// =========================================================
|
||
// Documentation text normalized.
|
||
// =========================================================
|
||
/// <summary>
|
||
/// Documentation text normalized.
|
||
/// </summary>
|
||
private void RecalculateTotalMaxHP()
|
||
{
|
||
totalMaxHP = 0;
|
||
if (recognizedEnemySOs != null)
|
||
{
|
||
foreach (var so in recognizedEnemySOs)
|
||
{
|
||
if (so != null) totalMaxHP += so.GetStatsByDifficultyID(CurrentDifficultyID).enemy_maxHP;
|
||
}
|
||
}
|
||
if (VerboseLogs) Debug.Log($"[teamUIController] Total Max HP has been recalculated to: {totalMaxHP}");
|
||
// Documentation text normalized.
|
||
UpdateAllEnemyTotalHealthUIImmediate();
|
||
}
|
||
|
||
private static void SetBarScaleY(Image img, float ratio01)
|
||
{
|
||
if (img == null) return;
|
||
var tr = img.transform;
|
||
if (tr == null) return;
|
||
var s = tr.localScale;
|
||
s.y = Mathf.Clamp01(ratio01);
|
||
tr.localScale = s;
|
||
}
|
||
|
||
private static float GetBarScaleY(Image img)
|
||
{
|
||
if (img == null) return 0f;
|
||
var tr = img.transform;
|
||
if (tr == null) return 0f;
|
||
return tr.localScale.y;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
private void UpdateAllEnemyTotalHealthUIOnHpChange(int newCurrentHP)
|
||
{
|
||
if (allEnemy_totalHealthImage == null) return;
|
||
|
||
// Documentation text normalized.
|
||
int totalCur = 0;
|
||
if (recognizedEnemySOs != null)
|
||
{
|
||
for (int i = enemyCurrentCount; i < recognizedEnemySOs.Length; i++)
|
||
{
|
||
var so = recognizedEnemySOs[i];
|
||
if (so == null) continue;
|
||
|
||
// Documentation text normalized.
|
||
if (i == enemyCurrentCount) totalCur += Mathf.Max(0, newCurrentHP);
|
||
else totalCur += Mathf.Max(0, so.GetStatsByDifficultyID(CurrentDifficultyID).enemy_maxHP);
|
||
}
|
||
}
|
||
|
||
float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f;
|
||
|
||
// Documentation text normalized.
|
||
allEnemy_totalHealthImage.fillAmount = newFill;
|
||
// SetBarScaleY(allEnemy_totalHealthImage, newFill);
|
||
// SetBarScaleY(allEnemy_totalHealthImage, newFill);
|
||
if (totalEnemy_healthRate != null)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(totalCur).Append("/").Append(totalMaxHP);
|
||
totalEnemy_healthRate.text = _sb.ToString();
|
||
}
|
||
|
||
// 2. Animate fade bar (only when health decreases)
|
||
if (allEnemy_totalFadehealthImage != null)
|
||
{
|
||
// float currentFadeFill = GetBarScaleY(allEnemy_totalFadehealthImage);
|
||
float currentFadeFill = allEnemy_totalFadehealthImage.fillAmount;
|
||
|
||
if (currentFadeFill > newFill)
|
||
{
|
||
// Health decreased: stop previous coroutine and start new fall animation
|
||
if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine);
|
||
if (Application.isPlaying)
|
||
totalFadeHealthCoroutine = StartCoroutine(TotalFadeHealthCoroutine(newFill));
|
||
else
|
||
{
|
||
allEnemy_totalFadehealthImage.fillAmount = newFill;
|
||
// SetBarScaleY(allEnemy_totalFadehealthImage, newFill);
|
||
}
|
||
}
|
||
else if (currentFadeFill < newFill)
|
||
{
|
||
// Health increased (healing): update fade bar immediately
|
||
if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine);
|
||
allEnemy_totalFadehealthImage.fillAmount = newFill;
|
||
// SetBarScaleY(allEnemy_totalFadehealthImage, newFill);
|
||
}
|
||
// If currentFadeFill == newFill, do nothing.
|
||
}
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
private void UpdateAllEnemyTotalHealthUIImmediate()
|
||
{
|
||
if (allEnemy_totalHealthImage == null) return;
|
||
|
||
int totalCur = 0;
|
||
if (recognizedEnemySOs != null)
|
||
{
|
||
for (int i = enemyCurrentCount; i < recognizedEnemySOs.Length; i++)
|
||
{
|
||
var so = recognizedEnemySOs[i];
|
||
if (so == null) continue;
|
||
|
||
// Documentation text normalized.
|
||
if (i == enemyCurrentCount)
|
||
{
|
||
// Documentation text normalized.
|
||
if (enemyCombatantInstance != null) totalCur += Mathf.Max(0, enemyCombatantInstance.currentHP);
|
||
else totalCur += Mathf.Max(0, so.GetStatsByDifficultyID(CurrentDifficultyID).enemy_maxHP); // Fallback
|
||
}
|
||
else
|
||
{
|
||
totalCur += Mathf.Max(0, so.GetStatsByDifficultyID(CurrentDifficultyID).enemy_maxHP);
|
||
}
|
||
}
|
||
}
|
||
|
||
float newFill = totalMaxHP > 0 ? (float)totalCur / totalMaxHP : 0f;
|
||
|
||
// Documentation text normalized.
|
||
allEnemy_totalHealthImage.fillAmount = newFill;
|
||
if (allEnemy_totalFadehealthImage != null)
|
||
{
|
||
allEnemy_totalFadehealthImage.fillAmount = newFill;
|
||
// SetBarScaleY(allEnemy_totalFadehealthImage, newFill);
|
||
}
|
||
if (totalEnemy_healthRate != null)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(totalCur).Append("/").Append(totalMaxHP);
|
||
totalEnemy_healthRate.text = _sb.ToString();
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
if (totalFadeHealthCoroutine != null) StopCoroutine(totalFadeHealthCoroutine);
|
||
}
|
||
|
||
|
||
private void UpdateEnemyUIImmediate()
|
||
{
|
||
if (enemyCombatantInstance == null) return;
|
||
// health
|
||
if (currentEnemy_healthImage != null)
|
||
currentEnemy_healthImage.fillAmount = enemyCombatantInstance.maxHP > 0 ? (float)enemyCombatantInstance.currentHP / enemyCombatantInstance.maxHP : 0f;
|
||
if (currentEnemy_fadehealthImage != null)
|
||
currentEnemy_fadehealthImage.fillAmount = currentEnemy_healthImage != null ? currentEnemy_healthImage.fillAmount : (enemyCombatantInstance.maxHP > 0 ? (float)enemyCombatantInstance.currentHP / enemyCombatantInstance.maxHP : 0f);
|
||
if (currentEnemy_healthRate != null)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(enemyCombatantInstance.currentHP).Append("/").Append(enemyCombatantInstance.maxHP);
|
||
currentEnemy_healthRate.text = _sb.ToString();
|
||
}
|
||
|
||
// mana visuals (new)
|
||
if (currentEnemy_manaImage != null)
|
||
currentEnemy_manaImage.fillAmount = enemyCombatantInstance.maxMana > 0 ? (float)enemyCombatantInstance.currentMana / enemyCombatantInstance.maxMana : 0f;
|
||
if (currentEnemy_fademanaImage != null)
|
||
currentEnemy_fademanaImage.fillAmount = currentEnemy_manaImage != null ? currentEnemy_manaImage.fillAmount : (enemyCombatantInstance.maxMana > 0 ? (float)enemyCombatantInstance.currentMana / enemyCombatantInstance.maxMana : 0f);
|
||
if (currentEnemy_manaRate != null)
|
||
{
|
||
if (enemyCombatantInstance.maxMana > 0)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(enemyCombatantInstance.currentMana).Append("/").Append(enemyCombatantInstance.maxMana);
|
||
currentEnemy_manaRate.text = _sb.ToString();
|
||
}
|
||
else
|
||
{
|
||
currentEnemy_manaRate.text = "0/0";
|
||
}
|
||
}
|
||
|
||
// Aggregate total health logic (modified to call the helper method for animation control)
|
||
UpdateAllEnemyTotalHealthUIImmediate();
|
||
}
|
||
|
||
private System.Text.StringBuilder _sb = new System.Text.StringBuilder();
|
||
|
||
private void UpdateEnemyHealthVisuals(int oldHP, int newHP, bool animateFade)
|
||
{
|
||
if (enemyCombatantInstance == null) return;
|
||
if (currentEnemy_healthImage != null)
|
||
{
|
||
float newFill = enemyCombatantInstance.maxHP > 0 ? (float)newHP / enemyCombatantInstance.maxHP : 0f;
|
||
currentEnemy_healthImage.fillAmount = Mathf.MoveTowards(currentEnemy_healthImage.fillAmount, newFill, 1f);
|
||
}
|
||
if (currentEnemy_healthRate != null)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(newHP).Append("/").Append(enemyCombatantInstance.maxHP);
|
||
currentEnemy_healthRate.text = _sb.ToString();
|
||
}
|
||
|
||
if (enemyFadeHealthCoroutine != null) StopCoroutine(enemyFadeHealthCoroutine);
|
||
if (currentEnemy_fadehealthImage != null)
|
||
{
|
||
float target = currentEnemy_healthImage != null ? currentEnemy_healthImage.fillAmount : (enemyCombatantInstance.maxHP > 0 ? (float)newHP / enemyCombatantInstance.maxHP : 0f);
|
||
if (animateFade && Application.isPlaying && gameObject.activeInHierarchy)
|
||
enemyFadeHealthCoroutine = StartCoroutine(EnemyFadeHealthCoroutine(target));
|
||
else
|
||
currentEnemy_fadehealthImage.fillAmount = target;
|
||
}
|
||
|
||
// hurt flash if hp decreased (Moved to GfxController.PlayHitFX for synchronized hit effects)
|
||
/*
|
||
if (oldHP >= 0 && newHP < oldHP && currentEnemy_hurtRedImage != null && Application.isPlaying)
|
||
{
|
||
StopCoroutine("EnemyHurtFlash");
|
||
StartCoroutine(EnemyHurtFlash());
|
||
}
|
||
*/
|
||
}
|
||
|
||
private void UpdateEnemyManaVisuals(int oldMana, int newMana, bool animateFade)
|
||
{
|
||
if (enemyCombatantInstance == null) return;
|
||
if (currentEnemy_manaRate != null)
|
||
{
|
||
if (enemyCombatantInstance.maxMana > 0)
|
||
{
|
||
_sb.Clear();
|
||
_sb.Append(newMana).Append("/").Append(enemyCombatantInstance.maxMana);
|
||
currentEnemy_manaRate.text = _sb.ToString();
|
||
}
|
||
else
|
||
{
|
||
currentEnemy_manaRate.text = "0/0";
|
||
}
|
||
}
|
||
|
||
if (enemyFadeManaCoroutine != null) StopCoroutine(enemyFadeManaCoroutine);
|
||
if (currentEnemy_fademanaImage != null)
|
||
{
|
||
float target = currentEnemy_manaImage != null ? currentEnemy_manaImage.fillAmount : (enemyCombatantInstance.maxMana > 0 ? (float)newMana / enemyCombatantInstance.maxMana : 0f);
|
||
if (animateFade && Application.isPlaying && gameObject.activeInHierarchy)
|
||
enemyFadeManaCoroutine = StartCoroutine(EnemyFadeManaCoroutine(target));
|
||
else if (currentEnemy_fademanaImage != null)
|
||
currentEnemy_fademanaImage.fillAmount = target;
|
||
}
|
||
|
||
// update main mana bar immediately
|
||
if (currentEnemy_manaImage != null)
|
||
{
|
||
currentEnemy_manaImage.fillAmount = enemyCombatantInstance.maxMana > 0 ? (float)newMana / enemyCombatantInstance.maxMana : 0f;
|
||
}
|
||
}
|
||
|
||
private IEnumerator EnemyFadeHealthCoroutine(float targetFill)
|
||
{
|
||
if (currentEnemy_fadehealthImage == null) yield break;
|
||
float start = currentEnemy_fadehealthImage.fillAmount;
|
||
float duration = 0.6f;
|
||
float t = 0f;
|
||
while (t < duration)
|
||
{
|
||
t += Time.deltaTime;
|
||
currentEnemy_fadehealthImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration);
|
||
yield return null;
|
||
}
|
||
currentEnemy_fadehealthImage.fillAmount = targetFill;
|
||
}
|
||
|
||
private IEnumerator EnemyFadeManaCoroutine(float targetFill)
|
||
{
|
||
if (currentEnemy_fademanaImage == null) yield break;
|
||
float start = currentEnemy_fademanaImage.fillAmount;
|
||
float duration = 0.4f;
|
||
float t = 0f;
|
||
while (t < duration)
|
||
{
|
||
t += Time.deltaTime;
|
||
currentEnemy_fademanaImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration);
|
||
yield return null;
|
||
}
|
||
currentEnemy_fademanaImage.fillAmount = targetFill;
|
||
}
|
||
|
||
public void TriggerEnemyHurtFlash()
|
||
{
|
||
// Don't flash if all enemies are defeated or if we are in the middle of a spine death transition
|
||
if (enemyCurrentCount >= recognizedEnemySOs.Length || _enemySpineDeathCoroutine != null) return;
|
||
|
||
if (_currentEnemySkeletonDataAsset != null && _enemySkeletonGraphic != null)
|
||
{
|
||
if (_spineFlashCoroutine != null) StopCoroutine(_spineFlashCoroutine);
|
||
_enemySkeletonGraphic.color = Color.white;
|
||
_enemySkeletonGraphic.Skeleton.SetColor(Color.white);
|
||
_enemySkeletonGraphic.UpdateMesh(true);
|
||
_spineFlashCoroutine = StartCoroutine(SpineHurtFlash());
|
||
return;
|
||
}
|
||
if (currentEnemy_hurtRedImage == null) return;
|
||
StopCoroutine("EnemyHurtFlash");
|
||
StartCoroutine(EnemyHurtFlash());
|
||
}
|
||
|
||
private IEnumerator EnemyHurtFlash()
|
||
{
|
||
if (currentEnemy_hurtRedImage == null) yield break;
|
||
Color c = currentEnemy_hurtRedImage.color;
|
||
c.a = 1f;
|
||
currentEnemy_hurtRedImage.color = c;
|
||
yield return new WaitForSeconds(0.02f);
|
||
float dur = 0.08f;
|
||
float t = 0f;
|
||
while (t < dur)
|
||
{
|
||
t += Time.deltaTime;
|
||
c.a = Mathf.Lerp(1f, 0f, t / dur);
|
||
currentEnemy_hurtRedImage.color = c;
|
||
yield return null;
|
||
}
|
||
c.a = 0f;
|
||
currentEnemy_hurtRedImage.color = c;
|
||
}
|
||
|
||
private IEnumerator SpineHurtFlash()
|
||
{
|
||
if (_enemySkeletonGraphic == null) yield break;
|
||
Color original = _enemySkeletonGraphic.color;
|
||
Color originalSkeleton = _enemySkeletonGraphic.Skeleton.GetColor();
|
||
Color target = spineHurtFlashColor;
|
||
target.a = 1f;
|
||
_enemySkeletonGraphic.color = target;
|
||
_enemySkeletonGraphic.Skeleton.SetColor(target);
|
||
_enemySkeletonGraphic.UpdateMesh(true);
|
||
yield return new WaitForSeconds(0.02f);
|
||
float dur = 0.08f;
|
||
float t = 0f;
|
||
while (t < dur)
|
||
{
|
||
t += Time.deltaTime;
|
||
Color lerped = Color.Lerp(target, original, t / dur);
|
||
_enemySkeletonGraphic.color = lerped;
|
||
_enemySkeletonGraphic.Skeleton.SetColor(lerped);
|
||
yield return null;
|
||
}
|
||
_enemySkeletonGraphic.color = original;
|
||
_enemySkeletonGraphic.Skeleton.SetColor(originalSkeleton);
|
||
_enemySkeletonGraphic.UpdateMesh(true);
|
||
_spineFlashCoroutine = null;
|
||
}
|
||
|
||
private void TryStartEnemySpineSpawnFade(EnemyData_SO so)
|
||
{
|
||
if (_spineSpawnFadeCoroutine != null)
|
||
{
|
||
StopCoroutine(_spineSpawnFadeCoroutine);
|
||
_spineSpawnFadeCoroutine = null;
|
||
}
|
||
if (!_pendingSpineSpawnFadeIn)
|
||
{
|
||
return;
|
||
}
|
||
_pendingSpineSpawnFadeIn = false;
|
||
if (so == null || so.skeletonDataAsset == null || _enemySkeletonGraphic == null)
|
||
{
|
||
return;
|
||
}
|
||
_spineSpawnFadeCoroutine = StartCoroutine(EnemySpineSpawnFade(SPINE_SPAWN_FADE_DURATION));
|
||
}
|
||
|
||
private IEnumerator EnemySpineSpawnFade(float duration)
|
||
{
|
||
if (_enemySkeletonGraphic == null)
|
||
{
|
||
_spineSpawnFadeCoroutine = null;
|
||
yield break;
|
||
}
|
||
var skeleton = _enemySkeletonGraphic.Skeleton;
|
||
if (skeleton == null)
|
||
{
|
||
_spineSpawnFadeCoroutine = null;
|
||
yield break;
|
||
}
|
||
Color baseColor = _enemySkeletonGraphic.color;
|
||
Color baseSkeleton = skeleton.GetColor();
|
||
baseColor.a = 1f;
|
||
baseSkeleton.a = 1f;
|
||
Color startColor = baseColor;
|
||
Color startSkeleton = baseSkeleton;
|
||
startColor.a = 0f;
|
||
startSkeleton.a = 0f;
|
||
_enemySkeletonGraphic.color = startColor;
|
||
skeleton.SetColor(startSkeleton);
|
||
_enemySkeletonGraphic.UpdateMesh(true);
|
||
if (duration <= 0f)
|
||
{
|
||
_enemySkeletonGraphic.color = baseColor;
|
||
skeleton.SetColor(baseSkeleton);
|
||
_enemySkeletonGraphic.UpdateMesh(true);
|
||
_spineSpawnFadeCoroutine = null;
|
||
yield break;
|
||
}
|
||
float t = 0f;
|
||
while (t < duration)
|
||
{
|
||
t += Time.deltaTime;
|
||
float alpha = Mathf.Clamp01(t / duration);
|
||
Color c = baseColor;
|
||
Color sc = baseSkeleton;
|
||
c.a = alpha;
|
||
sc.a = alpha;
|
||
_enemySkeletonGraphic.color = c;
|
||
skeleton.SetColor(sc);
|
||
_enemySkeletonGraphic.UpdateMesh(true);
|
||
yield return null;
|
||
}
|
||
_enemySkeletonGraphic.color = baseColor;
|
||
skeleton.SetColor(baseSkeleton);
|
||
_enemySkeletonGraphic.UpdateMesh(true);
|
||
_spineSpawnFadeCoroutine = null;
|
||
}
|
||
|
||
public void TriggerEnemySpineShake(Vector3 amplitude, Vector3 frequency, float duration)
|
||
{
|
||
if (_currentEnemySkeletonDataAsset == null) return;
|
||
if (spine_to_put == null) return;
|
||
SmoothShake ss = spine_to_put.GetComponent<SmoothShake>();
|
||
if (ss == null) ss = spine_to_put.gameObject.AddComponent<SmoothShake>();
|
||
if (ss.positionShake == null) ss.positionShake = new Shaker();
|
||
if (ss.rotationShake == null) ss.rotationShake = new Shaker();
|
||
Vector3 halfAmplitude = amplitude * 0.5f;
|
||
ss.positionShake.noiseType = Shaker.NoiseType.SineWave;
|
||
ss.positionShake.amplitude = halfAmplitude;
|
||
ss.positionShake.frequency = frequency;
|
||
ss.rotationShake.amplitude = Vector3.zero;
|
||
ss.timeSettings.constantShake = false;
|
||
ss.timeSettings.fadeInDuration = 0.05f;
|
||
ss.timeSettings.holdDuration = duration * 0.4f;
|
||
ss.timeSettings.fadeOutDuration = duration * 0.55f;
|
||
if (ss.timeSettings.fadeInCurve == null || ss.timeSettings.fadeInCurve.length == 0)
|
||
ss.timeSettings.fadeInCurve = AnimationCurve.Linear(0, 0, 1, 1);
|
||
if (ss.timeSettings.fadeOutCurve == null || ss.timeSettings.fadeOutCurve.length == 0)
|
||
ss.timeSettings.fadeOutCurve = AnimationCurve.Linear(0, 1, 1, 0);
|
||
ss.enabled = false;
|
||
ss.enabled = true;
|
||
}
|
||
|
||
public void TriggerAllyHurtFlash(int slotIndex)
|
||
{
|
||
Image img = null;
|
||
switch (slotIndex)
|
||
{
|
||
case 0: img = teammate01_hurtRedImage; break;
|
||
case 1: img = teammate02_hurtRedImage; break;
|
||
case 2: img = teammate03_hurtRedImage; break;
|
||
case 3: img = teammate04_hurtRedImage; break;
|
||
case 4: img = teammate05_hurtRedImage; break;
|
||
}
|
||
|
||
if (img == null) return;
|
||
if (gameObject.activeInHierarchy)
|
||
StartCoroutine(AllyHurtFlashCoroutine(img));
|
||
}
|
||
|
||
private IEnumerator AllyHurtFlashCoroutine(Image img)
|
||
{
|
||
if (img == null) yield break;
|
||
Color c = img.color;
|
||
c.a = 1f;
|
||
img.color = c;
|
||
yield return new WaitForSeconds(0.02f);
|
||
float dur = 0.08f;
|
||
float t = 0f;
|
||
while (t < dur)
|
||
{
|
||
t += Time.deltaTime;
|
||
c.a = Mathf.Lerp(1f, 0f, t / dur);
|
||
img.color = c;
|
||
yield return null;
|
||
}
|
||
c.a = 0f;
|
||
img.color = c;
|
||
}
|
||
|
||
private IEnumerator TotalFadeHealthCoroutine(float targetFill)
|
||
{
|
||
if (allEnemy_totalFadehealthImage == null) yield break;
|
||
|
||
// Delay to create the "fade" lag behind the top bar.
|
||
yield return new WaitForSeconds(0.5f);
|
||
if (allEnemy_totalFadehealthImage == null) yield break;
|
||
|
||
// float start = GetBarScaleY(allEnemy_totalFadehealthImage);
|
||
float start = allEnemy_totalFadehealthImage.fillAmount;
|
||
float duration = 0.6f;
|
||
float t = 0f;
|
||
while (t < duration)
|
||
{
|
||
t += Time.deltaTime;
|
||
float y = Mathf.Lerp(start, targetFill, t / duration);
|
||
allEnemy_totalFadehealthImage.fillAmount = y;
|
||
// SetBarScaleY(allEnemy_totalFadehealthImage, y);
|
||
yield return null;
|
||
}
|
||
allEnemy_totalFadehealthImage.fillAmount = targetFill;
|
||
// SetBarScaleY(allEnemy_totalFadehealthImage, targetFill);
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
// Documentation text normalized.
|
||
// Documentation text normalized.
|
||
public IEnumerator AllyImageFlash(Image hurtImage, Color targetColor)
|
||
{
|
||
if (hurtImage == null) yield break;
|
||
|
||
float startAlpha = flashStartAlpha;
|
||
Color c = targetColor;
|
||
c.a = startAlpha;
|
||
hurtImage.color = c;
|
||
|
||
yield return new WaitForSeconds(flashHoldTime);
|
||
|
||
float dur = flashFadeDuration;
|
||
float t = 0f;
|
||
while (t < dur)
|
||
{
|
||
t += Time.deltaTime;
|
||
c.a = Mathf.Lerp(startAlpha, 0f, t / dur);
|
||
hurtImage.color = c;
|
||
yield return null;
|
||
}
|
||
c.a = 0f;
|
||
hurtImage.color = c;
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
// Documentation text normalized.
|
||
public IEnumerator AllyDeathGrayscale(Image characterImage)
|
||
{
|
||
if (characterImage == null || grayScaleMaterial == null) yield break;
|
||
|
||
// Documentation text normalized.
|
||
Material instanceMat = new Material(grayScaleMaterial);
|
||
characterImage.material = instanceMat;
|
||
|
||
float dur = 1.0f;
|
||
float t = 0f;
|
||
while (t < dur)
|
||
{
|
||
t += Time.deltaTime;
|
||
float sat = Mathf.Lerp(1f, 0f, t / dur);
|
||
instanceMat.SetFloat(SaturationID, sat);
|
||
yield return null;
|
||
}
|
||
instanceMat.SetFloat(SaturationID, 0f);
|
||
}
|
||
|
||
// Documentation text normalized.
|
||
}
|