using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
[ExecuteAlways]
///
/// AllyCombatant �������Ѿ�������ʱ GameObject������Ϊ ally_01..ally_05����
/// ʵ�� ICombatant �ӿ��Թ� EffectSystem ���á�
/// - �� teamUIController ��ȡ UI ���ò�ͬ����ʾѪ��/����
/// - ����ʱ�� TeamCharacterDataInfo �� AllyHero_SO ����������/�����Ȼ�������
/// - ��ʼ����ֵΪ�������ֵ����ʼ����Ϊ 0
///
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 activeBuffs = new List();
// Track skills that are currently active because HP percent condition is satisfied.
private HashSet _activeHpPercentSkills = new HashSet();
// Track applied reversible effects for HP-percent skills so we can revert them on leave
private Dictionary _appliedHpPercentEffects = new Dictionary();
private class AppliedEffect
{
public EffectType effectType;
public float amount;
public AppliedEffect(EffectType t, float a) { effectType = t; amount = a; }
}
private void Awake()
{
// ensure GameObject name matches pattern so EffectSystem can find it
gameObject.name = $"ally_0{Mathf.Clamp(slotIndex + 1, 1, 5)}";
if (_activeHpPercentSkills == null) _activeHpPercentSkills = new HashSet();
if (_appliedHpPercentEffects == null) _appliedHpPercentEffects = new Dictionary();
}
private void Start()
{
// Resolve UI and optionally pull data from SO
ResolveUIReferences();
if (allowOverwriteFromSO)
InitializeStatsFromData();
// �ؼ������볡ʱӦ��Ѫ/0 ����
// ֮ǰ����ֻ�� Clamp���ᱣ�� prefab/������ inspector �ϵľ� currentHP�������볡����Ѫ��
isDead = false;
currentHP = Mathf.Max(1, maxHP);
currentMana = 0;
UpdateUIImmediate();
// Ensure score UI shows initial value
UpdateScoreUI();
// Evaluate HP-percent triggers on start after a short delay to ensure singletons are ready
if (Application.isPlaying)
{
StartCoroutine(InitialTriggerCheckCoroutine());
}
}
private IEnumerator InitialTriggerCheckCoroutine()
{
// Wait for singletons to be ready
while (SkillBuilder.Instance == null || teamUIController.Instance == null)
{
yield return null;
}
// Wait until teamUIController has finished loading ally SOs if they are async
// or just wait a couple of frames to be sure
yield return null;
yield return null;
// Re-run initialization to ensure we have the correct MaxHP and skills from the loaded team
if (allowOverwriteFromSO)
{
InitializeStatsFromData();
currentHP = Mathf.Max(1, maxHP);
UpdateUIImmediate();
}
// Final wait to ensure SkillBuilder's internal mapping is ready
while (SkillBuilder.Instance.GetAllyHeroSOBySlot(slotIndex) == null)
{
yield return null;
}
// Perform the initial HP-percentage trigger check (oldHP = currentHP to trigger "ENTER" if condition met)
EvaluateHPPercentageTriggers(currentHP);
}
// 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 currentHP or currentMana in inspector while in Play mode, attempt to trigger behaviors
if (Application.isPlaying && SkillBuilder.Instance != null)
{
EvaluateHPPercentageTriggers(currentHP);
TryCastOnFullMana();
}
}
private void ResolveUIReferences()
{
// Try singleton first, fallback to scene search (works in editor)
var ui = teamUIController.Instance ?? FindObjectOfType();
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(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(true);
if (legacy != null)
{
var go = legacy.gameObject;
var created = go.GetComponent() ?? go.AddComponent();
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("");
foreach (var a in all)
{
if (a != null && a.ally_heroID == id)
{
// �ؼ�����ͬһ�� AllyHero_SO �������λʹ��ʱ������ֱ���� a.ally_currentEXP����������
// ���Դ���Ҵ浵/PlayerPrefs ��ȡ������λ���ľ���ֵ��
// Լ��������selected_heroSlot0{slotIndex+1}_exp������ selected_heroSlot01_exp����
int expKeySlot = Mathf.Clamp(slotIndex + 1, 1, 5);
string expKey = $"selected_heroSlot0{expKeySlot}_exp";
int slotExp = a.ally_currentEXP;
if (PlayerPrefs.HasKey(expKey))
slotExp = PlayerPrefs.GetInt(expKey, a.ally_currentEXP);
// pick effective level based on per-slot EXP
if (a.levelStats != null && a.levelStats.Count > 0)
{
var eff = GetEffectiveLevelForExp(a, slotExp);
if (eff != null)
{
maxHP = eff.maxHP;
maxMana = eff.maxMana;
damageResistance = eff.damageResistance;
scoreEfficiency = eff.scoreEfficiency;
attack = eff.attack;
}
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;
}
}
}
}
// ���ݴ��� EXP ����ȼ������� SO �ϵ� ally_currentEXP����������λ����״̬��
private static AllyHero_SO.AllyLevelInfo GetEffectiveLevelForExp(AllyHero_SO so, int exp)
{
if (so == null || so.levelStats == null || so.levelStats.Count == 0) return null;
AllyHero_SO.AllyLevelInfo best = null;
foreach (var lvl in so.levelStats)
{
if (lvl == null) continue;
if (exp >= lvl.requiredEXP)
{
if (best == null || lvl.requiredEXP >= best.requiredEXP)
best = lvl;
}
}
return best ?? so.levelStats[0];
}
// 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);
// Trigger HP changed events
int delta = currentHP - old;
if (delta > 0)
{
TryTriggerSkillsOnEvent(SkillDefinition.SkillTrigger.OnHPHealed);
}
else if (delta < 0)
{
TryTriggerSkillsOnEvent(SkillDefinition.SkillTrigger.OnHPLost);
}
// Evaluate HP-percentage based trigger transitions (entry/exit)
EvaluateHPPercentageTriggers(old);
}
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;
int oldMax = maxHP;
int oldHP = currentHP;
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();
// Max HP change affects percentage triggers
EvaluateHPPercentageTriggers(oldHP);
}
public void SetCurrentMana(int mana, bool animateFade = true, bool flashVisuals = true)
{
if (isDead) return; // dead characters cannot change mana
int old = currentMana;
currentMana = Mathf.Clamp(mana, 0, maxMana);
UpdateManaVisuals(old, animateFade, flashVisuals);
TryCastOnFullMana();
// Trigger mana changed events
int delta = currentMana - old;
if (delta > 0)
{
TryTriggerSkillsOnEvent(SkillDefinition.SkillTrigger.OnManaGained);
}
else if (delta < 0)
{
TryTriggerSkillsOnEvent(SkillDefinition.SkillTrigger.OnManaLost);
}
}
public void ModifyMana(int delta, bool animateFade = true, bool flashVisuals = true)
{
if (isDead && delta > 0)
{
// dead units cannot gain mana
return;
}
SetCurrentMana(currentMana + delta, animateFade, flashVisuals);
}
public void SetMaxMana(int newMax, bool keepCurrentRatio = true)
{
if (newMax < 1) newMax = 1;
int oldMax = maxMana;
int oldMana = currentMana;
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();
// Mana doesn't currently have percentage triggers, but for consistency:
// EvaluateHPPercentageTriggers(currentHP); // Not needed for mana but keep in mind
}
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;
}
// HP change flash effects
if (hurtRedImage != null && Application.isPlaying)
{
var ui = teamUIController.Instance;
if (ui != null)
{
if (currentHP < oldHP)
{
// Damage flash (Red)
if (!isDead)
StartCoroutine(ui.AllyImageFlash(hurtRedImage, ui.hurtColor));
}
else if (currentHP > oldHP)
{
// Heal flash (Green)
StartCoroutine(ui.AllyImageFlash(hurtRedImage, ui.healColor));
}
}
}
// if dead, trigger grayscale effect on character image
if (isDead && characterImage != null && Application.isPlaying)
{
var ui = teamUIController.Instance;
if (ui != null)
{
// 移除原有的红闪常亮逻辑,改为调用黑白渐变效果
StartCoroutine(ui.AllyDeathGrayscale(characterImage));
}
}
}
private void UpdateManaVisuals(int oldMana, bool animateFade, bool flashVisuals = true)
{
// 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;
}
// Mana recovery flash effect
if (flashVisuals && currentMana > oldMana && hurtRedImage != null && Application.isPlaying)
{
var ui = teamUIController.Instance;
if (ui != null)
{
StartCoroutine(ui.AllyImageFlash(hurtRedImage, ui.manaColor));
}
}
}
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);
}
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 TryTriggerSkillsOnEvent(SkillDefinition.SkillTrigger when)
{
if (!Application.isPlaying) return;
if (SkillBuilder.Instance == null) return;
var so = SkillBuilder.Instance.GetAllyHeroSOBySlot(slotIndex);
if (so == null) return;
// If equipped groups exist, iterate them first
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
{
foreach (var gid in so.equippedSkillGroupIDs)
{
if (gid == 0) continue;
SkillGroup group = null;
for (int k = 0; k < so.skillGroups.Length; k++)
{
var g = so.skillGroups[k];
if (g != null && g.skillGroupID == gid) { group = g; break; }
}
if (group == null) continue;
foreach (var def in group.skills)
{
if (def == null) continue;
if (def.triggerCondition != when) continue;
SkillBuilder.Instance.UseSkillDefinition(def, slotIndex, -1f, null);
}
}
return;
}
// Fallback: check primary group
var fallbackGroup = so.GetPrimarySkillGroup();
if (fallbackGroup != null)
{
foreach (var def in fallbackGroup.skills)
{
if (def == null) continue;
if (def.triggerCondition != when) continue;
SkillBuilder.Instance.UseSkillDefinition(def, slotIndex, -1f, null);
}
return;
}
// Final fallback: iterate availableSkills
if (so.availableSkills != null)
{
foreach (var def in so.availableSkills)
{
if (def == null) continue;
if (def.triggerCondition != when) continue;
SkillBuilder.Instance.UseSkillDefinition(def, slotIndex, -1f, null);
}
}
}
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;
}
bool anyTriggered = false;
// First: equipped groups
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
{
foreach (int gid in so.equippedSkillGroupIDs)
{
if (gid == 0) continue;
SkillGroup group = null;
for (int k = 0; k < so.skillGroups.Length; k++) { var g = so.skillGroups[k]; if (g != null && g.skillGroupID == gid) { group = g; break; } }
if (group == null) continue;
foreach (var skill in group.skills)
{
if (skill == null) continue;
if (skill.triggerCondition != SkillDefinition.SkillTrigger.OnManaFull) continue;
SkillBuilder.Instance.UseSkillDefinition(skill, slotIndex, -1f, null);
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} cast skill from group: {skill.skillId} (OnManaFull)");
anyTriggered = true;
}
}
}
// Second: primary group
if (!anyTriggered)
{
var fallbackGroup = so.GetPrimarySkillGroup();
if (fallbackGroup != null)
{
foreach (var skill in fallbackGroup.skills)
{
if (skill == null) continue;
if (skill.triggerCondition != SkillDefinition.SkillTrigger.OnManaFull) continue;
SkillBuilder.Instance.UseSkillDefinition(skill, slotIndex, -1f, null);
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} cast skill from primary group: {skill.skillId} (OnManaFull)");
anyTriggered = true;
}
}
}
// Third: availableSkills / primary skill fallback
if (!anyTriggered)
{
// try primary skill specifically
var def = so.GetPrimarySkill();
if (def != null && def.triggerCondition == SkillDefinition.SkillTrigger.OnManaFull)
{
SkillBuilder.Instance.UseSkillDefinition(def, slotIndex, -1f, null);
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} cast primary skill '{def.skillId}' due to ManaFull.");
anyTriggered = true;
}
else if (so.availableSkills != null)
{
foreach (var skill in so.availableSkills)
{
if (skill == null) continue;
if (skill.triggerCondition != SkillDefinition.SkillTrigger.OnManaFull) continue;
SkillBuilder.Instance.UseSkillDefinition(skill, slotIndex, -1f, null);
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1} cast available skill: {skill.skillId} (OnManaFull)");
anyTriggered = true;
}
}
}
if (!anyTriggered)
{
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1}: no skills configured for OnManaFull.");
}
}
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);
}
// New: Evaluate HP-percent triggers and handle enter/leave transitions
private void EvaluateHPPercentageTriggers(int oldHP)
{
if (!Application.isPlaying) return;
if (SkillBuilder.Instance == null) return;
var so = SkillBuilder.Instance.GetAllyHeroSOBySlot(slotIndex);
if (so == null) return;
float oldPct = (maxHP > 0) ? (float)oldHP / (float)maxHP : 0f;
float currPct = (maxHP > 0) ? (float)currentHP / (float)maxHP : 0f;
Action processDef = (def) =>
{
if (def == null) return;
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnHPAbovePercent && def.triggerCondition != SkillDefinition.SkillTrigger.OnHPBelowPercent)
return;
bool wasActive = _activeHpPercentSkills.Contains(def.skillId);
bool nowActive = false;
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnHPAbovePercent)
nowActive = currPct >= def.hpTriggerPercent;
else if (def.triggerCondition == SkillDefinition.SkillTrigger.OnHPBelowPercent)
nowActive = currPct <= def.hpTriggerPercent;
// Evaluate formula into amount using similar variables as SkillBuilder
float amount = 0f;
var vars = new Dictionary();
vars["slot"] = slotIndex;
vars["attack"] = attack;
if (so != null && so.levelStats != null && so.levelStats.Count > 0)
{
var eff = so.GetEffectiveLevelForCurrentEXP();
var lvl = eff ?? so.levelStats[0];
vars["maxHP"] = lvl.maxHP;
vars["maxMana"] = lvl.maxMana;
vars["damageResistance"] = lvl.damageResistance;
vars["scoreEfficiency"] = lvl.scoreEfficiency;
vars["attack"] = lvl.attack;
}
else
{
vars["maxHP"] = maxHP;
vars["maxMana"] = maxMana;
vars["damageResistance"] = damageResistance;
vars["scoreEfficiency"] = scoreEfficiency;
}
vars["ally_currentEXP"] = so != null ? so.ally_currentEXP : 0f;
if (!string.IsNullOrWhiteSpace(def.formula))
{
if (!SkillDefinition.TryEvaluateFormula(def.formula, vars, out float fresult))
amount = 0f;
else
amount = fresult;
}
// Decide if this effect type is reversible (state-based)
bool isReversible = IsEffectReversible(def.effectType);
if (!isReversible)
{
// For non-reversible effects (like Damage or instant Heal), we don't track state
// Just trigger it once when the condition is first met
if (!wasActive && nowActive)
{
_activeHpPercentSkills.Add(def.skillId);
EffectSystem.Instance.ApplyEffect(def.defaultSelector, def.effectType, amount, 0f, this.gameObject, null);
}
else if (wasActive && !nowActive)
{
_activeHpPercentSkills.Remove(def.skillId);
}
return;
}
if (!wasActive && nowActive)
{
// ENTER condition: Apply effect and record amount
_activeHpPercentSkills.Add(def.skillId);
EffectSystem.Instance.ApplyEffect(def.defaultSelector, def.effectType, amount, 0f, this.gameObject, null);
_appliedHpPercentEffects[def.skillId] = new AppliedEffect(def.effectType, amount);
}
else if (wasActive && !nowActive)
{
// LEAVE condition: Revert effect using recorded amount
_activeHpPercentSkills.Remove(def.skillId);
if (_appliedHpPercentEffects.TryGetValue(def.skillId, out var applied))
{
ApplyInverseEffect(def.defaultSelector, applied.effectType, applied.amount);
_appliedHpPercentEffects.Remove(def.skillId);
}
}
};
// iterate equipped groups -> primary group -> availableSkills (same as other triggers)
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
{
foreach (var gid in so.equippedSkillGroupIDs)
{
if (gid == 0) continue;
SkillGroup group = null;
for (int k = 0; k < so.skillGroups.Length; k++)
{
var g = so.skillGroups[k];
if (g != null && g.skillGroupID == gid) { group = g; break; }
}
if (group == null) continue;
foreach (var def in group.skills)
{
processDef(def);
}
}
return;
}
var fallbackGroup = so.GetPrimarySkillGroup();
if (fallbackGroup != null)
{
foreach (var def in fallbackGroup.skills)
{
processDef(def);
}
return;
}
if (so.availableSkills != null)
{
foreach (var def in so.availableSkills)
{
processDef(def);
}
}
}
private bool IsEffectReversible(EffectType type)
{
switch (type)
{
case EffectType.IncreaseAttack:
case EffectType.DecreaseAttack:
case EffectType.IncreaseMaxHP:
case EffectType.DecreaseMaxHP:
case EffectType.IncreaseMaxMana:
case EffectType.DecreaseMaxMana:
case EffectType.IncreaseScoreEfficiency:
case EffectType.DecreaseScoreEfficiency:
case EffectType.IncreaseDamageResistance:
case EffectType.DecreaseDamageResistance:
case EffectType.ScoreMultiplier:
case EffectType.AddScore:
return true;
default:
return false;
}
}
private void ApplyInverseEffect(Selector selector, EffectType type, float amount)
{
if (EffectSystem.Instance == null) return;
switch (type)
{
case EffectType.IncreaseAttack:
EffectSystem.Instance.ApplyEffect(selector, EffectType.DecreaseAttack, amount, 0f, this.gameObject, null);
break;
case EffectType.DecreaseAttack:
EffectSystem.Instance.ApplyEffect(selector, EffectType.IncreaseAttack, amount, 0f, this.gameObject, null);
break;
case EffectType.IncreaseMaxHP:
EffectSystem.Instance.ApplyEffect(selector, EffectType.DecreaseMaxHP, amount, 0f, this.gameObject, null);
break;
case EffectType.DecreaseMaxHP:
EffectSystem.Instance.ApplyEffect(selector, EffectType.IncreaseMaxHP, amount, 0f, this.gameObject, null);
break;
case EffectType.IncreaseMaxMana:
EffectSystem.Instance.ApplyEffect(selector, EffectType.DecreaseMaxMana, amount, 0f, this.gameObject, null);
break;
case EffectType.DecreaseMaxMana:
EffectSystem.Instance.ApplyEffect(selector, EffectType.IncreaseMaxMana, amount, 0f, this.gameObject, null);
break;
case EffectType.IncreaseScoreEfficiency:
EffectSystem.Instance.ApplyEffect(selector, EffectType.DecreaseScoreEfficiency, amount, 0f, this.gameObject, null);
break;
case EffectType.DecreaseScoreEfficiency:
EffectSystem.Instance.ApplyEffect(selector, EffectType.IncreaseScoreEfficiency, amount, 0f, this.gameObject, null);
break;
case EffectType.IncreaseDamageResistance:
EffectSystem.Instance.ApplyEffect(selector, EffectType.DecreaseDamageResistance, amount, 0f, this.gameObject, null);
break;
case EffectType.DecreaseDamageResistance:
EffectSystem.Instance.ApplyEffect(selector, EffectType.IncreaseDamageResistance, amount, 0f, this.gameObject, null);
break;
case EffectType.ScoreMultiplier:
if (Mathf.Abs(amount) > 0.0001f)
EffectSystem.Instance.ApplyEffect(selector, EffectType.ScoreMultiplier, 1f / amount, 0f, this.gameObject, null);
break;
case EffectType.AddScore:
EffectSystem.Instance.ApplyEffect(selector, EffectType.AddScore, -amount, 0f, this.gameObject, null);
break;
default:
// Fallback: apply same type with negative amount if it's likely a numeric stat
EffectSystem.Instance.ApplyEffect(selector, type, -amount, 0f, this.gameObject, null);
break;
}
}
}