711 lines
27 KiB
C#
711 lines
27 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using TMPro;
|
|
|
|
[ExecuteAlways]
|
|
/// <summary>
|
|
/// AllyCombatant 绑定到场上友军的运行时 GameObject(命名为 ally_01..ally_05),
|
|
/// 实现 ICombatant 接口以供 EffectSystem 调用。
|
|
/// - 从 teamUIController 获取 UI 引用并同步显示血量/法力
|
|
/// - 启动时从 TeamCharacterDataInfo 或 AllyHero_SO 填充最大生命/法力等基础属性
|
|
/// - 初始生命值为最大生命值,初始法力为 0
|
|
/// </summary>
|
|
public class AllyCombatant : MonoBehaviour, ICombatant
|
|
{
|
|
public int slotIndex = 0; // 0..4 对应 ally_01..ally_05
|
|
|
|
// runtime stats
|
|
public int maxHP = 100;
|
|
public int currentHP = 100;
|
|
public int maxMana = 100;
|
|
public int currentMana = 0;
|
|
public float damageResistance = 0f; // 0..1
|
|
public float scoreEfficiency = 1f;
|
|
|
|
private bool isDead = false; // new: dead flag
|
|
public BeatmapManager bmm;
|
|
|
|
// add attack stat to be available at runtime
|
|
[Tooltip("基础攻击力(运行时使用),会从 AllyHero_SO.levelStats[0].attack 填充(如果存在)")]
|
|
public int attack = 0;
|
|
|
|
// scoring fields
|
|
[Header("Scoring")]
|
|
[Tooltip("Base score value for a perfect hit on this track")]
|
|
public int baseTrackScore = 1000;
|
|
|
|
// Editable multipliers for each judge quality (exposed for designers)
|
|
[Tooltip("Score multiplier for Perfect (default 1.0)")]
|
|
public float perfectRatio = 1f;
|
|
[Tooltip("Score multiplier for Great (e.g. 0.75)")]
|
|
public float greatRatio = 0.75f;
|
|
[Tooltip("Score multiplier for Good (e.g. 0.5)")]
|
|
public float goodRatio = 0.5f;
|
|
[Tooltip("Score multiplier for Miss (e.g. 0)")]
|
|
public float missRatio = 0f;
|
|
|
|
[Tooltip("角色当前轨道得分(累加),显示于 UI 的 currentScore 文本)")]
|
|
public int currentScore = 0;
|
|
|
|
[Tooltip("轨道分数上限,暂设为 int.MaxValue - 1")]
|
|
public int maxTrackScore = int.MaxValue - 1;
|
|
|
|
// When true, InitializeStatsFromData() will overwrite inspector values on Start.
|
|
// Set to false if you want to keep manual inspector values when entering Play mode.
|
|
public bool allowOverwriteFromSO = true;
|
|
|
|
// UI refs (resolved from teamUIController)
|
|
private Image characterImage;
|
|
private Image healthImage;
|
|
private Image fadeHealthImage;
|
|
private Image manaImage;
|
|
private Image fadeManaImage;
|
|
private TextMeshProUGUI nameText;
|
|
private TextMeshProUGUI healthRateText;
|
|
private TextMeshProUGUI currentScoreText;
|
|
private Image hurtRedImage;
|
|
|
|
private Coroutine fadeHealthCoroutine;
|
|
private Coroutine fadeManaCoroutine;
|
|
private List<Buff> activeBuffs = new List<Buff>();
|
|
|
|
private void Awake()
|
|
{
|
|
// ensure GameObject name matches pattern so EffectSystem can find it
|
|
gameObject.name = $"ally_0{Mathf.Clamp(slotIndex + 1, 1, 5)}";
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
// Resolve UI and optionally pull data from SO
|
|
ResolveUIReferences();
|
|
if (allowOverwriteFromSO)
|
|
InitializeStatsFromData();
|
|
|
|
// Ensure runtime invariants
|
|
currentHP = Mathf.Clamp(currentHP, 0, Mathf.Max(1, maxHP));
|
|
currentMana = Mathf.Clamp(currentMana, 0, Mathf.Max(1, maxMana));
|
|
|
|
UpdateUIImmediate();
|
|
// Ensure score UI shows initial value
|
|
UpdateScoreUI();
|
|
}
|
|
|
|
// Allow editing in inspector to immediately reflect on UI (editor only)
|
|
private void OnValidate()
|
|
{
|
|
// Avoid heavy editor-only operations, but resolve UI references so inspector changes show
|
|
ResolveUIReferences();
|
|
// Clamp values to valid ranges
|
|
if (maxHP < 1) maxHP = 1;
|
|
if (maxMana < 1) maxMana = 1;
|
|
currentHP = Mathf.Clamp(currentHP, 0, maxHP);
|
|
currentMana = Mathf.Clamp(currentMana, 0, maxMana);
|
|
|
|
// Do not start coroutines from OnValidate - update immediately
|
|
UpdateUIImmediate();
|
|
|
|
// If user adjusts currentMana in inspector while in Play mode, attempt to trigger mana-full behaviors
|
|
// Guard to avoid calling TryCastOnFullMana before singletons are initialized
|
|
if (Application.isPlaying && SkillBuilder.Instance != null)
|
|
{
|
|
TryCastOnFullMana();
|
|
}
|
|
}
|
|
|
|
private void ResolveUIReferences()
|
|
{
|
|
// Try singleton first, fallback to scene search (works in editor)
|
|
var ui = teamUIController.Instance ?? FindObjectOfType<teamUIController>();
|
|
if (ui == null) return;
|
|
switch (slotIndex)
|
|
{
|
|
case 0:
|
|
characterImage = ui.teammate01_characterImage;
|
|
healthImage = ui.teammate01_healthImage;
|
|
fadeHealthImage = ui.teammate01_fadehealthImage;
|
|
manaImage = ui.teammate01_manaImage;
|
|
fadeManaImage = ui.teammate01_fadeManaImage;
|
|
nameText = ui.teammate01_nameText;
|
|
healthRateText = ui.teammate01_healthRate;
|
|
currentScoreText = ui.teammate01_current_scoreText;
|
|
hurtRedImage = ui.teammate01_hurtRedImage;
|
|
break;
|
|
case 1:
|
|
characterImage = ui.teammate02_characterImage;
|
|
healthImage = ui.teammate02_healthImage;
|
|
fadeHealthImage = ui.teammate02_fadehealthImage;
|
|
manaImage = ui.teammate02_manaImage;
|
|
fadeManaImage = ui.teammate02_fadeManaImage;
|
|
nameText = ui.teammate02_nameText;
|
|
healthRateText = ui.teammate02_healthRate;
|
|
currentScoreText = ui.teammate02_current_scoreText;
|
|
hurtRedImage = ui.teammate02_hurtRedImage;
|
|
break;
|
|
case 2:
|
|
characterImage = ui.teammate03_characterImage;
|
|
healthImage = ui.teammate03_healthImage;
|
|
fadeHealthImage = ui.teammate03_fadehealthImage;
|
|
manaImage = ui.teammate03_manaImage;
|
|
fadeManaImage = ui.teammate03_fadeManaImage;
|
|
nameText = ui.teammate03_nameText;
|
|
healthRateText = ui.teammate03_healthRate;
|
|
currentScoreText = ui.teammate03_current_scoreText;
|
|
hurtRedImage = ui.teammate03_hurtRedImage;
|
|
break;
|
|
case 3:
|
|
characterImage = ui.teammate04_characterImage;
|
|
healthImage = ui.teammate04_healthImage;
|
|
fadeHealthImage = ui.teammate04_fadehealthImage;
|
|
manaImage = ui.teammate04_manaImage;
|
|
fadeManaImage = ui.teammate04_fadeManaImage;
|
|
nameText = ui.teammate04_nameText;
|
|
healthRateText = ui.teammate04_healthRate;
|
|
currentScoreText = ui.teammate04_current_scoreText;
|
|
hurtRedImage = ui.teammate04_hurtRedImage;
|
|
break;
|
|
case 4:
|
|
characterImage = ui.teammate05_characterImage;
|
|
healthImage = ui.teammate05_healthImage;
|
|
fadeHealthImage = ui.teammate05_fadehealthImage;
|
|
manaImage = ui.teammate05_manaImage;
|
|
fadeManaImage = ui.teammate05_fadeManaImage;
|
|
nameText = ui.teammate05_nameText;
|
|
healthRateText = ui.teammate05_healthRate;
|
|
currentScoreText = ui.teammate05_current_scoreText;
|
|
hurtRedImage = ui.teammate05_hurtRedImage;
|
|
break;
|
|
}
|
|
|
|
// If the assigned TMP field is null, attempt to resolve from the UI parent's object (teamUIController keeps objectFather references)
|
|
if (currentScoreText == null)
|
|
{
|
|
GameObject parent = null;
|
|
switch (slotIndex)
|
|
{
|
|
case 0: parent = ui.objectFather_ally01; break;
|
|
case 1: parent = ui.objectFather_ally02; break;
|
|
case 2: parent = ui.objectFather_ally03; break;
|
|
case 3: parent = ui.objectFather_ally04; break;
|
|
case 4: parent = ui.objectFather_ally05; break;
|
|
}
|
|
if (parent != null)
|
|
{
|
|
var tmp = parent.GetComponentInChildren<TextMeshProUGUI>(true);
|
|
if (tmp != null)
|
|
{
|
|
currentScoreText = tmp;
|
|
// also assign back to teamUIController so other systems can find it
|
|
switch (slotIndex)
|
|
{
|
|
case 0: ui.teammate01_current_scoreText = tmp; break;
|
|
case 1: ui.teammate02_current_scoreText = tmp; break;
|
|
case 2: ui.teammate03_current_scoreText = tmp; break;
|
|
case 3: ui.teammate04_current_scoreText = tmp; break;
|
|
case 4: ui.teammate05_current_scoreText = tmp; break;
|
|
}
|
|
Debug.Log($"[AllyCombatant] Resolved currentScoreText for slot {slotIndex+1} from parent {parent.name} -> {tmp.gameObject.name}");
|
|
}
|
|
else
|
|
{
|
|
// legacy UnityEngine.UI.Text fallback
|
|
var legacy = parent.GetComponentInChildren<Text>(true);
|
|
if (legacy != null)
|
|
{
|
|
var go = legacy.gameObject;
|
|
var created = go.GetComponent<TextMeshProUGUI>() ?? go.AddComponent<TextMeshProUGUI>();
|
|
created.text = legacy.text;
|
|
currentScoreText = created;
|
|
switch (slotIndex)
|
|
{
|
|
case 0: ui.teammate01_current_scoreText = created; break;
|
|
case 1: ui.teammate02_current_scoreText = created; break;
|
|
case 2: ui.teammate03_current_scoreText = created; break;
|
|
case 3: ui.teammate04_current_scoreText = created; break;
|
|
case 4: ui.teammate05_current_scoreText = created; break;
|
|
}
|
|
Debug.LogWarning($"[AllyCombatant] Found legacy Text for slot {slotIndex+1} under {parent.name}. Added/used TMP component on {go.name} and copied text.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitializeStatsFromData()
|
|
{
|
|
// Try TeamCharacterDataInfo first
|
|
var arr = teamUIController.Instance?.GetCurrentAllySOs();
|
|
if (arr != null && slotIndex >= 0 && slotIndex < arr.Length)
|
|
{
|
|
var so = arr[slotIndex];
|
|
if (so != null)
|
|
{
|
|
// TeamCharacterDataInfo has limited fields; use CharacterMaxHealth if present
|
|
if (so.CharacterMaxHealth > 0)
|
|
{
|
|
maxHP = so.CharacterMaxHealth;
|
|
}
|
|
if (!string.IsNullOrEmpty(so.CharacterName) && nameText != null)
|
|
{
|
|
nameText.text = so.CharacterName;
|
|
}
|
|
if (characterImage != null && so.CharacterImage_gameplay != null)
|
|
{
|
|
characterImage.sprite = so.CharacterImage_gameplay.sprite;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If more detailed info exists in AllyHero_SO, try to load by ally ID
|
|
int id = -1;
|
|
if (teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null && slotIndex < teamUIController.Instance.allySlotIds.Count)
|
|
id = teamUIController.Instance.allySlotIds[slotIndex];
|
|
|
|
if (id > 0)
|
|
{
|
|
var all = Resources.LoadAll<AllyHero_SO>("");
|
|
foreach (var a in all)
|
|
{
|
|
if (a != null && a.ally_heroID == id)
|
|
{
|
|
// pick effective level based on current EXP if available
|
|
if (a.levelStats != null && a.levelStats.Count > 0)
|
|
{
|
|
var eff = a.GetEffectiveLevelForCurrentEXP();
|
|
if (eff != null)
|
|
{
|
|
maxHP = eff.maxHP;
|
|
maxMana = eff.maxMana;
|
|
damageResistance = eff.damageResistance;
|
|
scoreEfficiency = eff.scoreEfficiency;
|
|
attack = eff.attack; // populate attack from SO effective level
|
|
}
|
|
else
|
|
{
|
|
var lvl = a.levelStats[0];
|
|
maxHP = lvl.maxHP;
|
|
maxMana = lvl.maxMana;
|
|
damageResistance = lvl.damageResistance;
|
|
scoreEfficiency = lvl.scoreEfficiency;
|
|
attack = lvl.attack;
|
|
}
|
|
}
|
|
|
|
if (nameText != null) nameText.text = a.ally_heroName;
|
|
if (characterImage != null && a.ally_heroProfile != null) characterImage.sprite = a.ally_heroProfile;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Scoring API
|
|
// Modified to return the amount added so callers can track per-track contributions
|
|
public int AddScoreForJudge(string judge)
|
|
{
|
|
int add = 0;
|
|
float multiplier = 0f;
|
|
switch (judge)
|
|
{
|
|
case "Perfect":
|
|
multiplier = perfectRatio;
|
|
break;
|
|
case "Great":
|
|
multiplier = greatRatio;
|
|
break;
|
|
case "Good":
|
|
multiplier = goodRatio;
|
|
break;
|
|
case "Miss":
|
|
multiplier = missRatio;
|
|
break;
|
|
default:
|
|
multiplier = 0f;
|
|
break;
|
|
}
|
|
|
|
// Use perNoteScore from BeatmapManager as the base score for each note hit
|
|
int baseScore = bmm != null ? bmm.perNoteScore : baseTrackScore;
|
|
add = Mathf.FloorToInt(baseScore * multiplier);
|
|
|
|
int before = currentScore;
|
|
currentScore = Mathf.Clamp(currentScore + add, 0, maxTrackScore);
|
|
int actuallyAdded = currentScore - before;
|
|
UpdateScoreUI();
|
|
// update global total
|
|
ScoreManager.Instance?.RecalculateTotal();
|
|
return actuallyAdded;
|
|
}
|
|
|
|
// Add an arbitrary score delta directly (used by skills to modify single judge or grant/penalize score)
|
|
public void AddScoreDirect(int delta)
|
|
{
|
|
currentScore = Mathf.Clamp(currentScore + delta, 0, maxTrackScore);
|
|
UpdateScoreUI();
|
|
ScoreManager.Instance?.RecalculateTotal();
|
|
}
|
|
|
|
private void UpdateScoreUI()
|
|
{
|
|
string value = $"{currentScore}/{maxTrackScore}";
|
|
if (currentScoreText != null)
|
|
{
|
|
currentScoreText.text = value;
|
|
Debug.Log($"[AllyCombatant] Updated slot {slotIndex+1} score UI -> {value} (target {currentScoreText.gameObject.name})");
|
|
return;
|
|
}
|
|
|
|
// As a fallback, try to write directly to teamUIController.Instance fields if available
|
|
var ui = teamUIController.Instance;
|
|
if (ui != null)
|
|
{
|
|
switch (slotIndex)
|
|
{
|
|
case 0:
|
|
if (ui.teammate01_current_scoreText != null) { ui.teammate01_current_scoreText.text = value; Debug.Log($"[AllyCombatant] Fallback wrote to ui.teammate01_current_scoreText: {value}"); }
|
|
break;
|
|
case 1:
|
|
if (ui.teammate02_current_scoreText != null) { ui.teammate02_current_scoreText.text = value; Debug.Log($"[AllyCombatant] Fallback wrote to ui.teammate02_current_scoreText: {value}"); }
|
|
break;
|
|
case 2:
|
|
if (ui.teammate03_current_scoreText != null) { ui.teammate03_current_scoreText.text = value; Debug.Log($"[AllyCombatant] Fallback wrote to ui.teammate03_current_scoreText: {value}"); }
|
|
break;
|
|
case 3:
|
|
if (ui.teammate04_current_scoreText != null) { ui.teammate04_current_scoreText.text = value; Debug.Log($"[AllyCombatant] Fallback wrote to ui.teammate04_current_scoreText: {value}"); }
|
|
break;
|
|
case 4:
|
|
if (ui.teammate05_current_scoreText != null) { ui.teammate05_current_scoreText.text = value; Debug.Log($"[AllyCombatant] Fallback wrote to ui.teammate05_current_scoreText: {value}"); }
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Public setters that update UI and optionally animate fade bars
|
|
public void SetCurrentHP(int hp, bool animateFade = true)
|
|
{
|
|
if (isDead) return; // dead characters cannot change HP
|
|
int old = currentHP;
|
|
currentHP = Mathf.Clamp(hp, 0, maxHP);
|
|
if (currentHP <= 0) isDead = true;
|
|
UpdateHealthVisuals(old, animateFade);
|
|
}
|
|
|
|
public void ModifyHP(int delta, bool animateFade = true)
|
|
{
|
|
if (isDead && delta > 0)
|
|
{
|
|
// cannot heal a dead unit
|
|
return;
|
|
}
|
|
SetCurrentHP(currentHP + delta, animateFade);
|
|
}
|
|
|
|
public void SetMaxHP(int newMax, bool keepCurrentRatio = true)
|
|
{
|
|
if (newMax < 1) newMax = 1;
|
|
float ratio = maxHP > 0 ? (float)currentHP / maxHP : 1f;
|
|
maxHP = newMax;
|
|
if (keepCurrentRatio)
|
|
currentHP = Mathf.Clamp(Mathf.RoundToInt(ratio * maxHP), 0, maxHP);
|
|
else
|
|
currentHP = Mathf.Clamp(currentHP, 0, maxHP);
|
|
UpdateUIImmediate();
|
|
}
|
|
|
|
public void SetCurrentMana(int mana, bool animateFade = true)
|
|
{
|
|
if (isDead) return; // dead characters cannot change mana
|
|
int old = currentMana;
|
|
currentMana = Mathf.Clamp(mana, 0, maxMana);
|
|
UpdateManaVisuals(old, animateFade);
|
|
TryCastOnFullMana();
|
|
}
|
|
|
|
public void ModifyMana(int delta, bool animateFade = true)
|
|
{
|
|
if (isDead && delta > 0)
|
|
{
|
|
// dead units cannot gain mana
|
|
return;
|
|
}
|
|
SetCurrentMana(currentMana + delta, animateFade);
|
|
}
|
|
|
|
public void SetMaxMana(int newMax, bool keepCurrentRatio = true)
|
|
{
|
|
if (newMax < 1) newMax = 1;
|
|
float ratio = maxMana > 0 ? (float)currentMana / maxMana : 1f;
|
|
maxMana = newMax;
|
|
if (keepCurrentRatio)
|
|
currentMana = Mathf.Clamp(Mathf.RoundToInt(ratio * maxMana), 0, maxMana);
|
|
else
|
|
currentMana = Mathf.Clamp(currentMana, 0, maxMana);
|
|
UpdateUIImmediate();
|
|
}
|
|
|
|
private void UpdateHealthVisuals(int oldHP, bool animateFade)
|
|
{
|
|
// main bar should snap quickly to new value, while fade bar lerps towards it
|
|
if (healthImage != null)
|
|
{
|
|
float newFill = maxHP > 0 ? (float)currentHP / maxHP : 0f;
|
|
// snap main bar quickly for visual pop
|
|
healthImage.fillAmount = Mathf.MoveTowards(healthImage.fillAmount, newFill, 1f);
|
|
}
|
|
|
|
if (healthRateText != null)
|
|
healthRateText.text = $"{currentHP}/{maxHP}";
|
|
|
|
if (fadeHealthCoroutine != null) StopCoroutine(fadeHealthCoroutine);
|
|
if (fadeHealthImage != null)
|
|
{
|
|
float target = healthImage != null ? healthImage.fillAmount : maxHP > 0 ? (float)currentHP / maxHP : 0f;
|
|
if (animateFade && Application.isPlaying)
|
|
fadeHealthCoroutine = StartCoroutine(FadeHealthCoroutine(target));
|
|
else
|
|
fadeHealthImage.fillAmount = target;
|
|
}
|
|
|
|
// if HP decreased, show hurt flash; if dead, keep hurt overlay visible
|
|
if (currentHP < oldHP && hurtRedImage != null && Application.isPlaying)
|
|
{
|
|
StopCoroutine("HurtFlash");
|
|
StartCoroutine(HurtFlash());
|
|
}
|
|
if (isDead && hurtRedImage != null)
|
|
{
|
|
Color c = hurtRedImage.color;
|
|
c.a = 1f;
|
|
hurtRedImage.color = c; // keep overlay visible when dead
|
|
}
|
|
}
|
|
|
|
private void UpdateManaVisuals(int oldMana, bool animateFade)
|
|
{
|
|
// main bar snaps, fade bar lerps to new value for visual effect
|
|
if (manaImage != null)
|
|
{
|
|
float newFill = maxMana > 0 ? (float)currentMana / maxMana : 0f;
|
|
manaImage.fillAmount = Mathf.MoveTowards(manaImage.fillAmount, newFill, 1f);
|
|
}
|
|
|
|
if (fadeManaCoroutine != null) StopCoroutine(fadeManaCoroutine);
|
|
if (fadeManaImage != null)
|
|
{
|
|
float target = manaImage != null ? manaImage.fillAmount : maxMana > 0 ? (float)currentMana / maxMana : 0f;
|
|
if (animateFade && Application.isPlaying)
|
|
fadeManaCoroutine = StartCoroutine(FadeManaCoroutine(target));
|
|
else
|
|
fadeManaImage.fillAmount = target;
|
|
}
|
|
}
|
|
|
|
private void UpdateUIImmediate()
|
|
{
|
|
if (healthImage != null)
|
|
{
|
|
healthImage.fillAmount = maxHP > 0 ? (float)currentHP / maxHP : 0f;
|
|
}
|
|
if (fadeHealthImage != null)
|
|
{
|
|
// keep fade bar equal to main bar immediately
|
|
fadeHealthImage.fillAmount = healthImage != null ? healthImage.fillAmount : maxHP > 0 ? (float)currentHP / maxHP : 0f;
|
|
}
|
|
if (manaImage != null)
|
|
{
|
|
manaImage.fillAmount = maxMana > 0 ? (float)currentMana / maxMana : 0f;
|
|
}
|
|
if (fadeManaImage != null)
|
|
{
|
|
fadeManaImage.fillAmount = manaImage != null ? manaImage.fillAmount : maxMana > 0 ? (float)currentMana / maxMana : 0f;
|
|
}
|
|
if (healthRateText != null)
|
|
{
|
|
healthRateText.text = $"{currentHP}/{maxHP}";
|
|
}
|
|
}
|
|
|
|
private IEnumerator FadeHealthCoroutine(float targetFill)
|
|
{
|
|
if (fadeHealthImage == null) yield break;
|
|
float start = fadeHealthImage.fillAmount;
|
|
float duration = 0.6f;
|
|
float t = 0f;
|
|
while (t < duration)
|
|
{
|
|
t += Time.deltaTime;
|
|
fadeHealthImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration);
|
|
yield return null;
|
|
}
|
|
fadeHealthImage.fillAmount = targetFill;
|
|
}
|
|
|
|
private IEnumerator FadeManaCoroutine(float targetFill)
|
|
{
|
|
if (fadeManaImage == null) yield break;
|
|
float start = fadeManaImage.fillAmount;
|
|
float duration = 0.4f;
|
|
float t = 0f;
|
|
while (t < duration)
|
|
{
|
|
t += Time.deltaTime;
|
|
fadeManaImage.fillAmount = Mathf.Lerp(start, targetFill, t / duration);
|
|
yield return null;
|
|
}
|
|
fadeManaImage.fillAmount = targetFill;
|
|
}
|
|
|
|
public void ReceiveDamage(float amount, GameObject source)
|
|
{
|
|
float effective = amount * (1f - damageResistance);
|
|
int delta = Mathf.CeilToInt(effective);
|
|
ModifyHP(-delta, true);
|
|
}
|
|
|
|
private IEnumerator HurtFlash()
|
|
{
|
|
if (hurtRedImage == null) yield break;
|
|
Color c = hurtRedImage.color;
|
|
c.a = 1f;
|
|
hurtRedImage.color = c;
|
|
// when injured, remain visible a bit longer; if dead, keep it on
|
|
if (isDead)
|
|
{
|
|
yield break;
|
|
}
|
|
yield return new WaitForSeconds(0.2f);
|
|
float dur = 0.4f;
|
|
float t = 0f;
|
|
while (t < dur)
|
|
{
|
|
t += Time.deltaTime;
|
|
c.a = Mathf.Lerp(1f, 0f, t / dur);
|
|
hurtRedImage.color = c;
|
|
yield return null;
|
|
}
|
|
c.a = 0f;
|
|
hurtRedImage.color = c;
|
|
}
|
|
|
|
public void ReceiveHeal(float amount, GameObject source)
|
|
{
|
|
int delta = Mathf.CeilToInt(amount);
|
|
ModifyHP(delta, true);
|
|
}
|
|
|
|
public void ApplyBuff(Buff buff, GameObject source)
|
|
{
|
|
if (buff == null) return;
|
|
activeBuffs.Add(buff);
|
|
// simple immediate apply: multiply stats
|
|
if (buff.attackMultiplier != 1f) { attack = Mathf.RoundToInt(attack * buff.attackMultiplier); }
|
|
if (buff.scoreMultiplier != 1f) scoreEfficiency *= buff.scoreMultiplier;
|
|
// UI/visual could be added
|
|
}
|
|
|
|
public void RemoveBuff(string buffId)
|
|
{
|
|
var b = activeBuffs.Find(x => x.buffId == buffId);
|
|
if (b != null)
|
|
{
|
|
activeBuffs.Remove(b);
|
|
if (b.scoreMultiplier != 1f) scoreEfficiency /= b.scoreMultiplier;
|
|
// cannot easily restore attack without storing original; keep simple
|
|
}
|
|
}
|
|
|
|
private void TryCastOnFullMana()
|
|
{
|
|
if (currentMana >= maxMana && Application.isPlaying)
|
|
{
|
|
// Ensure SkillBuilder singleton exists before attempting to cast.
|
|
if (SkillBuilder.Instance == null)
|
|
{
|
|
Debug.LogWarning($"[AllyCombatant] Slot {slotIndex + 1} cannot cast right now: SkillBuilder.Instance is null. Will not attempt cast.");
|
|
return;
|
|
}
|
|
|
|
int prevMana = currentMana;
|
|
// Attempt to cast primary skill configured in SO via SkillBuilder
|
|
CastSkill();
|
|
// Reset mana after casting
|
|
currentMana = 0;
|
|
// Update visuals using previous mana as old value so fade works
|
|
UpdateManaVisuals(prevMana, true);
|
|
}
|
|
}
|
|
|
|
private void CastSkill()
|
|
{
|
|
// Use SkillBuilder to invoke the primary skill defined in the AllyHero_SO for this slot
|
|
if (SkillBuilder.Instance == null)
|
|
{
|
|
Debug.LogWarning($"[AllyCombatant] Slot {slotIndex + 1} cannot cast: SkillBuilder.Instance is null.");
|
|
return;
|
|
}
|
|
|
|
var so = SkillBuilder.Instance.GetAllyHeroSOBySlot(slotIndex);
|
|
if (so == null)
|
|
{
|
|
// Can't locate SO for this slot; attempt best-effort: call UsePrimarySkillForSlot which will log details
|
|
Debug.LogWarning($"[AllyCombatant] Slot {slotIndex + 1}: AllyHero_SO not found for slot. Falling back to UsePrimarySkillForSlot.");
|
|
SkillBuilder.Instance.UsePrimarySkillForSlot(slotIndex, -1f, null);
|
|
return;
|
|
}
|
|
|
|
var def = so.GetPrimarySkill();
|
|
if (def == null)
|
|
{
|
|
Debug.LogWarning($"[AllyCombatant] Slot {slotIndex + 1} cannot cast: primary skill not set in SO.");
|
|
return;
|
|
}
|
|
|
|
// Only auto-cast here if the skill's trigger condition matches ManaFull
|
|
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnManaFull)
|
|
{
|
|
// Use the SkillDefinition API to ensure selector/resolution/operateDirectly are respected
|
|
SkillBuilder.Instance.UseSkillDefinition(def, slotIndex, -1f, null);
|
|
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} cast primary skill '{def.skillId}' due to ManaFull.");
|
|
}
|
|
else
|
|
{
|
|
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} primary skill '{def.skillId}' not set to trigger on ManaFull (trigger={def.triggerCondition}); skipping cast.");
|
|
}
|
|
}
|
|
|
|
private void CastSkillGroup(SkillGroup group, int slotIndex)
|
|
{
|
|
if (group == null) return;
|
|
// For auto-casting triggered by ManaFull, only cast skills that are configured to trigger on ManaFull.
|
|
for (int i = 0; i < group.skills.Length; i++)
|
|
{
|
|
var skill = group.skills[i];
|
|
if (skill == null) continue;
|
|
// Only cast skills whose triggerCondition matches OnManaFull when this method is invoked from auto-cast path
|
|
if (skill.triggerCondition != SkillDefinition.SkillTrigger.OnManaFull)
|
|
{
|
|
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} skill '{skill.skillId}' skipped: triggerCondition={skill.triggerCondition}.");
|
|
continue;
|
|
}
|
|
SkillBuilder.Instance.UseSkillDefinition(skill, slotIndex, -1f, null);
|
|
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} cast skill from group: {skill.skillId}");
|
|
}
|
|
}
|
|
|
|
public void TriggerOnManaFull()
|
|
{
|
|
if (currentMana >= maxMana)
|
|
{
|
|
TryCastOnFullMana();
|
|
}
|
|
}
|
|
|
|
[ContextMenu("Test Fill Mana")]
|
|
private void TestFillMana()
|
|
{
|
|
SetCurrentMana(maxMana, true);
|
|
}
|
|
}
|