技能主要更新,修复卡顿并加入动画,以及各种其他更新。
This commit is contained in:
@@ -11,7 +11,7 @@ using TMPro;
|
||||
/// ʵ�� ICombatant �ӿ��Թ� EffectSystem ���á�
|
||||
/// - �� teamUIController ��ȡ UI ���ò�ͬ����ʾѪ��/����
|
||||
/// - ����ʱ�� TeamCharacterDataInfo �� AllyHero_SO ����������/�����Ȼ�������
|
||||
/// - ��ʼ����ֵΪ�������ֵ����ʼ����Ϊ 0
|
||||
/// - ��ʼ����ֵΪ�������ֵ����ʼ�����?0
|
||||
/// </summary>
|
||||
public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
@@ -28,8 +28,12 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
private bool isDead = false; // new: dead flag
|
||||
public BeatmapManager bmm;
|
||||
|
||||
// --- Damage redirect (next hit) ---
|
||||
private static AllyCombatant s_nextDamageRedirector;
|
||||
private static float s_redirectExpireTime = -1f;
|
||||
|
||||
// add attack stat to be available at runtime
|
||||
[Tooltip("����������������ʱʹ�ã������ AllyHero_SO.levelStats[0].attack ��䣨������ڣ�")]
|
||||
[Tooltip("����������������ʱʹ�ã������?AllyHero_SO.levelStats[0].attack ��䣨������ڣ�")]
|
||||
public int attack = 0;
|
||||
|
||||
// scoring fields
|
||||
@@ -47,10 +51,10 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
[Tooltip("Score multiplier for Miss (e.g. 0)")]
|
||||
public float missRatio = 0f;
|
||||
|
||||
[Tooltip("��ɫ��ǰ����÷֣��ۼӣ�����ʾ�� UI �� currentScore �ı���")]
|
||||
[Tooltip("��ɫ��ǰ����÷֣��ۼӣ�����ʾ��?UI �� currentScore �ı���")]
|
||||
public int currentScore = 0;
|
||||
|
||||
[Tooltip("����������ޣ�����Ϊ int.MaxValue - 1")]
|
||||
[Tooltip("����������ޣ������?int.MaxValue - 1")]
|
||||
public int maxTrackScore = int.MaxValue - 1;
|
||||
|
||||
// When true, InitializeStatsFromData() will overwrite inspector values on Start.
|
||||
@@ -78,6 +82,14 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
// Track applied reversible effects for HP-percent skills so we can revert them on leave
|
||||
private Dictionary<string, AppliedEffect> _appliedHpPercentEffects = new Dictionary<string, AppliedEffect>();
|
||||
|
||||
// Track skills that are currently active because Attack == 0
|
||||
private HashSet<string> _activeAttackZeroSkills = new HashSet<string>();
|
||||
private Dictionary<string, AppliedEffect> _appliedAttackZeroEffects = new Dictionary<string, AppliedEffect>();
|
||||
|
||||
// Reusable buffers to reduce allocations during skill evaluation
|
||||
private readonly List<SkillDefinition> _tmpSkillDefs = new List<SkillDefinition>(16);
|
||||
private readonly Dictionary<string, float> _formulaVarsBuffer = new Dictionary<string, float>(16);
|
||||
|
||||
private class AppliedEffect
|
||||
{
|
||||
public EffectType effectType;
|
||||
@@ -91,6 +103,13 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
gameObject.name = $"ally_0{Mathf.Clamp(slotIndex + 1, 1, 5)}";
|
||||
if (_activeHpPercentSkills == null) _activeHpPercentSkills = new HashSet<string>();
|
||||
if (_appliedHpPercentEffects == null) _appliedHpPercentEffects = new Dictionary<string, AppliedEffect>();
|
||||
if (_activeAttackZeroSkills == null) _activeAttackZeroSkills = new HashSet<string>();
|
||||
if (_appliedAttackZeroEffects == null) _appliedAttackZeroEffects = new Dictionary<string, AppliedEffect>();
|
||||
}
|
||||
|
||||
private void LogVerbose(string message)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log(message);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
@@ -166,6 +185,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (Application.isPlaying && SkillBuilder.Instance != null)
|
||||
{
|
||||
EvaluateHPPercentageTriggers(currentHP);
|
||||
EvaluateAttackZeroTriggers();
|
||||
TryCastOnFullMana();
|
||||
}
|
||||
}
|
||||
@@ -173,7 +193,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
private void ResolveUIReferences()
|
||||
{
|
||||
// Try singleton first, fallback to scene search (works in editor)
|
||||
var ui = teamUIController.Instance ?? FindObjectOfType<teamUIController>();
|
||||
var ui = teamUIController.Instance != null ? teamUIController.Instance : UnityEngine.Object.FindAnyObjectByType<teamUIController>();
|
||||
if (ui == null) return;
|
||||
switch (slotIndex)
|
||||
{
|
||||
@@ -261,7 +281,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
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}");
|
||||
LogVerbose($"[AllyCombatant] Resolved currentScoreText for slot {slotIndex+1} from parent {parent.name} -> {tmp.gameObject.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -325,8 +345,8 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
if (a != null && a.ally_heroID == id)
|
||||
{
|
||||
// �ؼ�����ͬһ�� AllyHero_SO �������λʹ��ʱ������ֱ���� a.ally_currentEXP����������
|
||||
// ���Դ���Ҵ浵/PlayerPrefs ��ȡ������λ���ľ���ֵ��
|
||||
// �ؼ�����ͬһ�� 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";
|
||||
@@ -344,7 +364,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
maxMana = eff.maxMana;
|
||||
damageResistance = eff.damageResistance;
|
||||
scoreEfficiency = eff.scoreEfficiency;
|
||||
attack = eff.attack;
|
||||
SetAttack(eff.attack);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -353,7 +373,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
maxMana = lvl.maxMana;
|
||||
damageResistance = lvl.damageResistance;
|
||||
scoreEfficiency = lvl.scoreEfficiency;
|
||||
attack = lvl.attack;
|
||||
SetAttack(lvl.attack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +385,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
}
|
||||
|
||||
// ���ݴ��� EXP ����ȼ������� SO �ϵ� ally_currentEXP����������λ����״̬��
|
||||
// ���ݴ��� 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;
|
||||
@@ -417,6 +437,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
UpdateScoreUI();
|
||||
// update global total
|
||||
ScoreManager.Instance?.RecalculateTotal();
|
||||
EvaluateAttackZeroTriggers();
|
||||
return actuallyAdded;
|
||||
}
|
||||
|
||||
@@ -434,7 +455,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (currentScoreText != null)
|
||||
{
|
||||
currentScoreText.text = value;
|
||||
Debug.Log($"[AllyCombatant] Updated slot {slotIndex+1} score UI -> {value} (target {currentScoreText.gameObject.name})");
|
||||
LogVerbose($"[AllyCombatant] Updated slot {slotIndex+1} score UI -> {value} (target {currentScoreText.gameObject.name})");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -445,24 +466,33 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
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}"); }
|
||||
if (ui.teammate01_current_scoreText != null) { ui.teammate01_current_scoreText.text = value; LogVerbose($"[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}"); }
|
||||
if (ui.teammate02_current_scoreText != null) { ui.teammate02_current_scoreText.text = value; LogVerbose($"[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}"); }
|
||||
if (ui.teammate03_current_scoreText != null) { ui.teammate03_current_scoreText.text = value; LogVerbose($"[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}"); }
|
||||
if (ui.teammate04_current_scoreText != null) { ui.teammate04_current_scoreText.text = value; LogVerbose($"[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}"); }
|
||||
if (ui.teammate05_current_scoreText != null) { ui.teammate05_current_scoreText.text = value; LogVerbose($"[AllyCombatant] Fallback wrote to ui.teammate05_current_scoreText: {value}"); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set max score cap for this track and refresh UI
|
||||
public void SetMaxTrackScore(int newMax, bool clampCurrent = true)
|
||||
{
|
||||
maxTrackScore = Mathf.Max(0, newMax);
|
||||
if (clampCurrent)
|
||||
currentScore = Mathf.Clamp(currentScore, 0, maxTrackScore);
|
||||
UpdateScoreUI();
|
||||
}
|
||||
|
||||
// Public setters that update UI and optionally animate fade bars
|
||||
public void SetCurrentHP(int hp, bool animateFade = true)
|
||||
{
|
||||
@@ -562,6 +592,22 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
// EvaluateHPPercentageTriggers(currentHP); // Not needed for mana but keep in mind
|
||||
}
|
||||
|
||||
public void ModifyAttack(int delta)
|
||||
{
|
||||
int old = attack;
|
||||
attack = Mathf.Max(0, attack + delta);
|
||||
if (old != attack)
|
||||
EvaluateAttackZeroTriggers();
|
||||
}
|
||||
|
||||
public void SetAttack(int value)
|
||||
{
|
||||
int old = attack;
|
||||
attack = Mathf.Max(0, value);
|
||||
if (old != attack)
|
||||
EvaluateAttackZeroTriggers();
|
||||
}
|
||||
|
||||
private void UpdateHealthVisuals(int oldHP, bool animateFade)
|
||||
{
|
||||
// main bar should snap quickly to new value, while fade bar lerps towards it
|
||||
@@ -611,7 +657,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null)
|
||||
{
|
||||
// 移除原有的红闪常亮逻辑,改为调用黑白渐变效果
|
||||
// 移除原有的红闪常亮逻辑,改为调用黑白渐变效�?
|
||||
StartCoroutine(ui.AllyDeathGrayscale(characterImage));
|
||||
}
|
||||
}
|
||||
@@ -704,6 +750,23 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
|
||||
public void ReceiveDamage(float amount, GameObject source)
|
||||
{
|
||||
ReceiveDamageInternal(amount, source, true);
|
||||
}
|
||||
|
||||
private void ReceiveDamageInternal(float amount, GameObject source, bool allowRedirect)
|
||||
{
|
||||
if (allowRedirect && IsRedirectActive())
|
||||
{
|
||||
var redirector = s_nextDamageRedirector;
|
||||
ClearRedirect();
|
||||
if (redirector != null && redirector != this)
|
||||
{
|
||||
redirector.ReceiveDamageInternal(amount, source, false);
|
||||
return;
|
||||
}
|
||||
// If redirector is self or invalid, consume redirect and continue to take damage normally.
|
||||
}
|
||||
|
||||
float effective = amount * (1f - damageResistance);
|
||||
int delta = Mathf.CeilToInt(effective);
|
||||
ModifyHP(-delta, true);
|
||||
@@ -720,7 +783,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
if (buff == null) return;
|
||||
activeBuffs.Add(buff);
|
||||
// simple immediate apply: multiply stats
|
||||
if (buff.attackMultiplier != 1f) { attack = Mathf.RoundToInt(attack * buff.attackMultiplier); }
|
||||
if (buff.attackMultiplier != 1f) { SetAttack(Mathf.RoundToInt(attack * buff.attackMultiplier)); }
|
||||
if (buff.scoreMultiplier != 1f) scoreEfficiency *= buff.scoreMultiplier;
|
||||
// UI/visual could be added
|
||||
}
|
||||
@@ -847,7 +910,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
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)");
|
||||
LogVerbose($"[AllyCombatant] Slot {slotIndex + 1} cast skill from group: {skill.skillId} (OnManaFull)");
|
||||
anyTriggered = true;
|
||||
}
|
||||
}
|
||||
@@ -864,7 +927,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
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)");
|
||||
LogVerbose($"[AllyCombatant] Slot {slotIndex + 1} cast skill from primary group: {skill.skillId} (OnManaFull)");
|
||||
anyTriggered = true;
|
||||
}
|
||||
}
|
||||
@@ -878,7 +941,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
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.");
|
||||
LogVerbose($"[AllyCombatant] Slot {slotIndex + 1} cast primary skill '{def.skillId}' due to ManaFull.");
|
||||
anyTriggered = true;
|
||||
}
|
||||
else if (so.availableSkills != null)
|
||||
@@ -888,7 +951,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
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)");
|
||||
LogVerbose($"[AllyCombatant] Slot {slotIndex + 1} cast available skill: {skill.skillId} (OnManaFull)");
|
||||
anyTriggered = true;
|
||||
}
|
||||
}
|
||||
@@ -896,7 +959,7 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
|
||||
if (!anyTriggered)
|
||||
{
|
||||
Debug.Log($"[AllyCombatant] Slot {slotIndex + 1}: no skills configured for OnManaFull.");
|
||||
LogVerbose($"[AllyCombatant] Slot {slotIndex + 1}: no skills configured for OnManaFull.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -911,11 +974,11 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
// 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}.");
|
||||
LogVerbose($"[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}");
|
||||
LogVerbose($"[AllyCombatant] Slot {slotIndex + 1} cast skill from group: {skill.skillId}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,7 +996,46 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
SetCurrentMana(maxMana, true);
|
||||
}
|
||||
|
||||
// New: Evaluate HP-percent triggers and handle enter/leave transitions
|
||||
|
||||
private Dictionary<string, float> BuildFormulaVars(AllyHero_SO so)
|
||||
{
|
||||
var vars = _formulaVarsBuffer;
|
||||
vars.Clear();
|
||||
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;
|
||||
vars["level"] = lvl.levelID;
|
||||
vars["levelID"] = lvl.levelID;
|
||||
vars["currentLevel"] = lvl.levelID;
|
||||
}
|
||||
else
|
||||
{
|
||||
vars["maxHP"] = maxHP;
|
||||
vars["maxMana"] = maxMana;
|
||||
vars["damageResistance"] = damageResistance;
|
||||
vars["scoreEfficiency"] = scoreEfficiency;
|
||||
vars["level"] = 0f;
|
||||
vars["levelID"] = 0f;
|
||||
vars["currentLevel"] = 0f;
|
||||
}
|
||||
|
||||
vars["ally_currentEXP"] = so != null ? so.ally_currentEXP : 0f;
|
||||
vars["currentMana"] = currentMana;
|
||||
vars["currentScore"] = currentScore;
|
||||
vars["idolScore"] = currentScore;
|
||||
vars["currentHP"] = currentHP;
|
||||
return vars;
|
||||
}
|
||||
// New: Evaluate HP-percent triggers and handle enter/leave transitions
|
||||
private void EvaluateHPPercentageTriggers(int oldHP)
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
@@ -945,6 +1047,77 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
float oldPct = (maxHP > 0) ? (float)oldHP / (float)maxHP : 0f;
|
||||
float currPct = (maxHP > 0) ? (float)currentHP / (float)maxHP : 0f;
|
||||
|
||||
// Collect candidate skills (same priority order as other triggers)
|
||||
var defs = _tmpSkillDefs;
|
||||
defs.Clear();
|
||||
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 || group.skills == null) continue;
|
||||
for (int i = 0; i < group.skills.Length; i++)
|
||||
{
|
||||
var def = group.skills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var fallbackGroup = so.GetPrimarySkillGroup();
|
||||
if (fallbackGroup != null && fallbackGroup.skills != null)
|
||||
{
|
||||
for (int i = 0; i < fallbackGroup.skills.Length; i++)
|
||||
{
|
||||
var def = fallbackGroup.skills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
else if (so.availableSkills != null)
|
||||
{
|
||||
for (int i = 0; i < so.availableSkills.Length; i++)
|
||||
{
|
||||
var def = so.availableSkills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select a single "best" Above/Below skill to avoid stacking when multiple thresholds are satisfied.
|
||||
string selectedAbove = null;
|
||||
float selectedAboveThreshold = float.MinValue;
|
||||
string selectedBelow = null;
|
||||
float selectedBelowThreshold = float.MaxValue;
|
||||
for (int i = 0; i < defs.Count; i++)
|
||||
{
|
||||
var def = defs[i];
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnHPAbovePercent)
|
||||
{
|
||||
if (currPct >= def.hpTriggerPercent && def.hpTriggerPercent >= selectedAboveThreshold)
|
||||
{
|
||||
selectedAboveThreshold = def.hpTriggerPercent;
|
||||
selectedAbove = def.skillId;
|
||||
}
|
||||
}
|
||||
else if (def.triggerCondition == SkillDefinition.SkillTrigger.OnHPBelowPercent)
|
||||
{
|
||||
if (currPct <= def.hpTriggerPercent && def.hpTriggerPercent <= selectedBelowThreshold)
|
||||
{
|
||||
selectedBelowThreshold = def.hpTriggerPercent;
|
||||
selectedBelow = def.skillId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var vars = BuildFormulaVars(so);
|
||||
Action<SkillDefinition> processDef = (def) =>
|
||||
{
|
||||
if (def == null) return;
|
||||
@@ -954,33 +1127,12 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
bool wasActive = _activeHpPercentSkills.Contains(def.skillId);
|
||||
bool nowActive = false;
|
||||
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnHPAbovePercent)
|
||||
nowActive = currPct >= def.hpTriggerPercent;
|
||||
nowActive = (selectedAbove != null && def.skillId == selectedAbove) && currPct >= def.hpTriggerPercent;
|
||||
else if (def.triggerCondition == SkillDefinition.SkillTrigger.OnHPBelowPercent)
|
||||
nowActive = currPct <= def.hpTriggerPercent;
|
||||
nowActive = (selectedBelow != null && def.skillId == selectedBelow) && currPct <= def.hpTriggerPercent;
|
||||
|
||||
// Evaluate formula into amount using similar variables as SkillBuilder
|
||||
float amount = 0f;
|
||||
var vars = new Dictionary<string, float>();
|
||||
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))
|
||||
{
|
||||
@@ -1028,7 +1180,22 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
};
|
||||
|
||||
// iterate equipped groups -> primary group -> availableSkills (same as other triggers)
|
||||
for (int i = 0; i < defs.Count; i++)
|
||||
processDef(defs[i]);
|
||||
}
|
||||
|
||||
private void EvaluateAttackZeroTriggers()
|
||||
{
|
||||
if (!Application.isPlaying) return;
|
||||
if (SkillBuilder.Instance == null) return;
|
||||
|
||||
var so = SkillBuilder.Instance.GetAllyHeroSOBySlot(slotIndex);
|
||||
if (so == null) return;
|
||||
|
||||
bool nowActive = attack <= 0;
|
||||
|
||||
var defs = _tmpSkillDefs;
|
||||
defs.Clear();
|
||||
if (so.equippedSkillGroupIDs != null && so.equippedSkillGroupIDs.Length > 0 && so.skillGroups != null)
|
||||
{
|
||||
foreach (var gid in so.equippedSkillGroupIDs)
|
||||
@@ -1040,32 +1207,119 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
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 (group == null || group.skills == null) continue;
|
||||
for (int i = 0; i < group.skills.Length; i++)
|
||||
{
|
||||
processDef(def);
|
||||
var def = group.skills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var fallbackGroup = so.GetPrimarySkillGroup();
|
||||
if (fallbackGroup != null)
|
||||
else
|
||||
{
|
||||
foreach (var def in fallbackGroup.skills)
|
||||
var fallbackGroup = so.GetPrimarySkillGroup();
|
||||
if (fallbackGroup != null && fallbackGroup.skills != null)
|
||||
{
|
||||
processDef(def);
|
||||
for (int i = 0; i < fallbackGroup.skills.Length; i++)
|
||||
{
|
||||
var def = fallbackGroup.skills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (so.availableSkills != null)
|
||||
{
|
||||
foreach (var def in so.availableSkills)
|
||||
else if (so.availableSkills != null)
|
||||
{
|
||||
processDef(def);
|
||||
for (int i = 0; i < so.availableSkills.Length; i++)
|
||||
{
|
||||
var def = so.availableSkills[i];
|
||||
if (def != null) defs.Add(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var vars = BuildFormulaVars(so);
|
||||
|
||||
for (int i = 0; i < defs.Count; i++)
|
||||
{
|
||||
var def = defs[i];
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnAttackZero) continue;
|
||||
|
||||
bool wasActive = _activeAttackZeroSkills.Contains(def.skillId);
|
||||
|
||||
// Evaluate formula into amount using similar variables as SkillBuilder
|
||||
float amount = 0f;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(def.formula))
|
||||
{
|
||||
if (!SkillDefinition.TryEvaluateFormula(def.formula, vars, out float fresult))
|
||||
amount = 0f;
|
||||
else
|
||||
amount = fresult;
|
||||
}
|
||||
|
||||
bool isReversible = IsEffectReversible(def.effectType);
|
||||
if (!isReversible)
|
||||
{
|
||||
if (!wasActive && nowActive)
|
||||
{
|
||||
_activeAttackZeroSkills.Add(def.skillId);
|
||||
EffectSystem.Instance.ApplyEffect(def.defaultSelector, def.effectType, amount, 0f, this.gameObject, null);
|
||||
}
|
||||
else if (wasActive && !nowActive)
|
||||
{
|
||||
_activeAttackZeroSkills.Remove(def.skillId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!wasActive && nowActive)
|
||||
{
|
||||
_activeAttackZeroSkills.Add(def.skillId);
|
||||
EffectSystem.Instance.ApplyEffect(def.defaultSelector, def.effectType, amount, 0f, this.gameObject, null);
|
||||
_appliedAttackZeroEffects[def.skillId] = new AppliedEffect(def.effectType, amount);
|
||||
}
|
||||
else if (wasActive && !nowActive)
|
||||
{
|
||||
_activeAttackZeroSkills.Remove(def.skillId);
|
||||
if (_appliedAttackZeroEffects.TryGetValue(def.skillId, out var applied))
|
||||
{
|
||||
ApplyInverseEffect(def.defaultSelector, applied.effectType, applied.amount);
|
||||
_appliedAttackZeroEffects.Remove(def.skillId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ActivateNextDamageRedirect(AllyCombatant redirector, float duration = 0f)
|
||||
{
|
||||
if (redirector == null) return;
|
||||
s_nextDamageRedirector = redirector;
|
||||
if (duration > 0f && Application.isPlaying)
|
||||
s_redirectExpireTime = Time.time + duration;
|
||||
else
|
||||
s_redirectExpireTime = -1f;
|
||||
}
|
||||
|
||||
private static bool IsRedirectActive()
|
||||
{
|
||||
if (s_nextDamageRedirector == null) return false;
|
||||
if (Application.isPlaying && s_redirectExpireTime > 0f && Time.time > s_redirectExpireTime)
|
||||
{
|
||||
ClearRedirect();
|
||||
return false;
|
||||
}
|
||||
if (s_nextDamageRedirector != null && s_nextDamageRedirector.isDead)
|
||||
{
|
||||
ClearRedirect();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ClearRedirect()
|
||||
{
|
||||
s_nextDamageRedirector = null;
|
||||
s_redirectExpireTime = -1f;
|
||||
}
|
||||
|
||||
private bool IsEffectReversible(EffectType type)
|
||||
@@ -1140,3 +1394,17 @@ public class AllyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ public enum EffectType
|
||||
IncreaseDamageResistance,
|
||||
DecreaseDamageResistance,
|
||||
IncreaseAttack,
|
||||
DecreaseAttack
|
||||
DecreaseAttack,
|
||||
RedirectNextDamageToSelf
|
||||
}
|
||||
|
||||
public interface ICombatant
|
||||
|
||||
@@ -6,31 +6,42 @@ using UnityEngine;
|
||||
/// <summary>
|
||||
/// EffectSystem �ṩ����Ч����ͳһ�ַ��ӿڡ�
|
||||
/// - ѡ������Χ��Selector������Ŀ�꼯�ϣ��Լ���ȫ���Ѿ������Լ����Ѿ��������Ѿ������ϵ��ˡ������ˣ�
|
||||
/// - Ч�����ͣ�EffectType�������˼�ʱ/�������˺�/����/����/����ȡ�
|
||||
/// - Ч�����ͣ�EffectType�������˼�ʱ/�������˺�/����/����/����ȡ�?
|
||||
///
|
||||
/// Ŀ��ʵ��Ӧʵ�� ICombatant �ӿ��Ա� EffectSystem �ܵ���ͳһ�����������˺�/����/����ȣ����û��ʵ�֣�������־�����ѡ�
|
||||
///
|
||||
/// �ýű�Ϊ��ܣ����������弼�����ã�ֻ�ṩ ApplyEffect(...) �ȷ����������ű����á�
|
||||
/// �ýű�Ϊ��ܣ����������弼�����ã�ֻ��?ApplyEffect(...) �ȷ����������ű����á�
|
||||
/// </summary>
|
||||
public class EffectSystem : MonoBehaviour
|
||||
{
|
||||
public static EffectSystem Instance { get; private set; }
|
||||
|
||||
// Per-frame caches to avoid repeated Find calls when multiple skills trigger in the same frame
|
||||
private int _cachedAlliesFrame = -1;
|
||||
private readonly List<GameObject> _cachedAllies = new List<GameObject>(8);
|
||||
private int _cachedEnemiesFrame = -1;
|
||||
private readonly List<GameObject> _cachedEnemies = new List<GameObject>(8);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null) Instance = this;
|
||||
else Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void LogVerbose(string message)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ApplyEffect: ��ָ��ѡ������Χ��Ŀ��Ӧ��Ч����
|
||||
/// - selector: Ŀ��ѡ����
|
||||
/// - effectType: Ч�����ͣ���ʱ/�����ȣ�
|
||||
/// - amount: ��ֵ���˺�������������Ч��ǿ�ȣ�
|
||||
/// - duration: ����ʱ�䣨�Գ���Ч���� Buff/Debuff ��Ч������ʱЧ����Ϊ 0
|
||||
/// - source: �����ߣ����� Self/AlliesExceptSelf �жϻ��¼��
|
||||
/// - specificTarget: �� selector Ϊ CurrentEnemies ��ֻ��ָ������ʱ�ɴ������Ŀ��
|
||||
/// - tickInterval: ����Ч���� tick ������룩��Ĭ�� 1 ��
|
||||
/// - source: �����ߣ����� Self/AlliesExceptSelf �жϻ��¼��?
|
||||
/// - specificTarget: �� selector Ϊ CurrentEnemies ��ֻ��ָ������ʱ�ɴ������Ŀ��?
|
||||
/// - tickInterval: ����Ч���� tick ������룩��Ĭ��?1 ��
|
||||
/// </summary>
|
||||
public void ApplyEffect(Selector selector, EffectType effectType, float amount, float duration = 0f, GameObject source = null, GameObject specificTarget = null, float tickInterval = 1f)
|
||||
{
|
||||
@@ -288,7 +299,7 @@ public class EffectSystem : MonoBehaviour
|
||||
int delta = Mathf.CeilToInt(amount);
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ally.attack += delta;
|
||||
ally.ModifyAttack(delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -316,7 +327,7 @@ public class EffectSystem : MonoBehaviour
|
||||
int delta = Mathf.CeilToInt(amount);
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ally.attack = Mathf.Max(0, ally.attack - delta);
|
||||
ally.ModifyAttack(-delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -335,6 +346,24 @@ public class EffectSystem : MonoBehaviour
|
||||
}
|
||||
break;
|
||||
|
||||
case EffectType.RedirectNextDamageToSelf:
|
||||
{
|
||||
AllyCombatant redirector = null;
|
||||
if (source != null)
|
||||
redirector = source.GetComponent<AllyCombatant>() ?? source.GetComponentInChildren<AllyCombatant>(true);
|
||||
if (redirector == null && targets != null && targets.Count > 0)
|
||||
redirector = targets[0].GetComponent<AllyCombatant>() ?? targets[0].GetComponentInChildren<AllyCombatant>(true);
|
||||
if (redirector != null)
|
||||
{
|
||||
AllyCombatant.ActivateNextDamageRedirect(redirector, duration);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[EffectSystem] RedirectNextDamageToSelf: no AllyCombatant found to apply.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EffectType.IncreaseDamageResistance:
|
||||
foreach (var t in targets)
|
||||
{
|
||||
@@ -346,7 +375,7 @@ public class EffectSystem : MonoBehaviour
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ally.damageResistance = Mathf.Clamp01(ally.damageResistance + amount);
|
||||
Debug.Log($"[EffectSystem] IncreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
|
||||
LogVerbose($"[EffectSystem] IncreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -358,7 +387,7 @@ public class EffectSystem : MonoBehaviour
|
||||
if (duration <= 0f)
|
||||
{
|
||||
enemyR.damageResistance = Mathf.Clamp01(enemyR.damageResistance + amount);
|
||||
Debug.Log($"[EffectSystem] IncreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR.damageResistance}");
|
||||
LogVerbose($"[EffectSystem] IncreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR.damageResistance}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -378,7 +407,7 @@ public class EffectSystem : MonoBehaviour
|
||||
if (duration <= 0f)
|
||||
{
|
||||
ally.damageResistance = Mathf.Clamp01(ally.damageResistance - amount);
|
||||
Debug.Log($"[EffectSystem] DecreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
|
||||
LogVerbose($"[EffectSystem] DecreaseDamageResistance applied to {t.name}: new resistance={ally.damageResistance}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -391,7 +420,7 @@ public class EffectSystem : MonoBehaviour
|
||||
if (duration <= 0f)
|
||||
{
|
||||
enemyR2.damageResistance = Mathf.Clamp01(enemyR2.damageResistance - amount);
|
||||
Debug.Log($"[EffectSystem] DecreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR2.damageResistance}");
|
||||
LogVerbose($"[EffectSystem] DecreaseDamageResistance applied to enemy {t.name}: new resistance={enemyR2.damageResistance}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -418,7 +447,7 @@ public class EffectSystem : MonoBehaviour
|
||||
List<GameObject> list = new List<GameObject>();
|
||||
|
||||
// prefer teamUIController when available for authoritative ally/enemy objects
|
||||
var ui = teamUIController.Instance ?? FindObjectOfType<teamUIController>();
|
||||
var ui = teamUIController.Instance != null ? teamUIController.Instance : UnityEngine.Object.FindAnyObjectByType<teamUIController>();
|
||||
|
||||
switch (selector)
|
||||
{
|
||||
@@ -562,14 +591,17 @@ public class EffectSystem : MonoBehaviour
|
||||
if (uniq.Count == 0)
|
||||
Debug.LogWarning($"[EffectSystem] ResolveTargets -> selector={selector} returned 0 targets (source={(source ? source.name : "null")}, specificTarget={(specificTarget ? specificTarget.name : "null")})");
|
||||
else
|
||||
Debug.Log($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
|
||||
LogVerbose($"[EffectSystem] ResolveTargets -> selector={selector} resolved {uniq.Count} targets: {string.Join(",", uniq.ConvertAll(x => x != null ? x.name : "null"))}");
|
||||
|
||||
return uniq;
|
||||
}
|
||||
|
||||
private IEnumerable<GameObject> FindAllAllies()
|
||||
{
|
||||
List<GameObject> allies = new List<GameObject>();
|
||||
if (_cachedAlliesFrame == Time.frameCount) return _cachedAllies;
|
||||
_cachedAlliesFrame = Time.frameCount;
|
||||
_cachedAllies.Clear();
|
||||
|
||||
// prefer teamUIController if available to get authoritative ally objects
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null)
|
||||
@@ -580,7 +612,7 @@ public class EffectSystem : MonoBehaviour
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var go = ui.GetAllyObjectBySlot(i);
|
||||
if (go != null && !allies.Contains(go)) allies.Add(go);
|
||||
if (go != null && !_cachedAllies.Contains(go)) _cachedAllies.Add(go);
|
||||
}
|
||||
}
|
||||
catch { /* ignore and fallback */ }
|
||||
@@ -591,7 +623,7 @@ public class EffectSystem : MonoBehaviour
|
||||
{
|
||||
string name = $"ally_0{i}"; // ally_01..ally_05
|
||||
var go = GameObject.Find(name);
|
||||
if (go != null && !allies.Contains(go)) allies.Add(go);
|
||||
if (go != null && !_cachedAllies.Contains(go)) _cachedAllies.Add(go);
|
||||
}
|
||||
|
||||
// also include objects tagged "Ally"
|
||||
@@ -599,52 +631,54 @@ public class EffectSystem : MonoBehaviour
|
||||
{
|
||||
var tagged = GameObject.FindGameObjectsWithTag("Ally");
|
||||
foreach (var g in tagged)
|
||||
if (!allies.Contains(g)) allies.Add(g);
|
||||
if (g != null && !_cachedAllies.Contains(g)) _cachedAllies.Add(g);
|
||||
}
|
||||
catch { /* tag might not exist; ignore */ }
|
||||
|
||||
return allies;
|
||||
return _cachedAllies;
|
||||
}
|
||||
|
||||
private IEnumerable<GameObject> FindAllEnemies()
|
||||
{
|
||||
List<GameObject> enemies = new List<GameObject>();
|
||||
if (_cachedEnemiesFrame == Time.frameCount) return _cachedEnemies;
|
||||
_cachedEnemiesFrame = Time.frameCount;
|
||||
_cachedEnemies.Clear();
|
||||
|
||||
// Prefer explicit EnemyCombatant components (more robust than tags)
|
||||
try
|
||||
{
|
||||
var comps = GameObject.FindObjectsOfType<EnemyCombatant>(true);
|
||||
var comps = UnityEngine.Object.FindObjectsByType<EnemyCombatant>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
foreach (var c in comps)
|
||||
{
|
||||
if (c != null && c.gameObject != null && !enemies.Contains(c.gameObject)) enemies.Add(c.gameObject);
|
||||
if (c != null && c.gameObject != null && !_cachedEnemies.Contains(c.gameObject)) _cachedEnemies.Add(c.gameObject);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Also include a single named instance if present
|
||||
var single = GameObject.Find("thisEnemy");
|
||||
if (single != null && !enemies.Contains(single)) enemies.Add(single);
|
||||
if (single != null && !_cachedEnemies.Contains(single)) _cachedEnemies.Add(single);
|
||||
|
||||
// Finally include any objects tagged "Enemy" (if tag exists)
|
||||
try
|
||||
{
|
||||
var tagged = GameObject.FindGameObjectsWithTag("Enemy");
|
||||
foreach (var g in tagged)
|
||||
if (!enemies.Contains(g)) enemies.Add(g);
|
||||
if (g != null && !_cachedEnemies.Contains(g)) _cachedEnemies.Add(g);
|
||||
}
|
||||
catch { /* ignore missing tag */ }
|
||||
|
||||
return enemies;
|
||||
return _cachedEnemies;
|
||||
}
|
||||
|
||||
#region Instant / OverTime Helpers
|
||||
private bool AreAllEnemiesDead()
|
||||
{
|
||||
var enemies = GameObject.FindObjectsOfType<EnemyCombatant>(true);
|
||||
if (enemies.Length == 0) return true;
|
||||
|
||||
foreach (var e in enemies)
|
||||
var enemies = FindAllEnemies();
|
||||
foreach (var go in enemies)
|
||||
{
|
||||
if (go == null) continue;
|
||||
var e = go.GetComponent<EnemyCombatant>();
|
||||
if (e != null && !e.IsDead && e.currentHP > 0)
|
||||
{
|
||||
return false;
|
||||
@@ -660,7 +694,7 @@ public class EffectSystem : MonoBehaviour
|
||||
// If target is an enemy, check if all enemies are already dead
|
||||
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
|
||||
{
|
||||
Debug.Log($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
|
||||
LogVerbose($"[EffectSystem] Skip ApplyInstantDamage to {target.name} because all enemies are dead.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -731,7 +765,7 @@ public class EffectSystem : MonoBehaviour
|
||||
// check death state for immediate damage
|
||||
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
|
||||
{
|
||||
Debug.Log($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead.");
|
||||
LogVerbose($"[EffectSystem] Skip DOT (immediate) to {target.name} because all enemies are dead.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -758,7 +792,7 @@ public class EffectSystem : MonoBehaviour
|
||||
// check death state inside loop
|
||||
if (target.GetComponent<EnemyCombatant>() != null && AreAllEnemiesDead())
|
||||
{
|
||||
Debug.Log($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
|
||||
LogVerbose($"[EffectSystem] Stopping DOT to {target.name} because all enemies are dead.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -937,9 +971,9 @@ public class EffectSystem : MonoBehaviour
|
||||
private IEnumerator ApplyTemporaryAttackChangeCoroutine(AllyCombatant ally, int delta, float duration)
|
||||
{
|
||||
if (ally == null) yield break;
|
||||
ally.attack = Mathf.Max(0, ally.attack + delta);
|
||||
ally.ModifyAttack(delta);
|
||||
yield return new WaitForSeconds(duration);
|
||||
ally.attack = Mathf.Max(0, ally.attack - delta);
|
||||
ally.ModifyAttack(-delta);
|
||||
}
|
||||
|
||||
// Coroutine to apply temporary attack change for EnemyCombatant
|
||||
@@ -952,3 +986,12 @@ public class EffectSystem : MonoBehaviour
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
// EnemyCombatant 是单个场上敌人的运行时表现(所有敌人共用同一个 GameObject,按顺序登场)
|
||||
// 实现 ICombatant:被 EffectSystem/SkillBuilder 调用
|
||||
// EnemyCombatant �ǵ������ϵ��˵�����ʱ���֣����е��˹���ͬһ�� GameObject����˳��dz���
|
||||
// ʵ�� ICombatant���� EffectSystem/SkillBuilder ����
|
||||
public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
public EnemyData_SO sourceData;
|
||||
@@ -30,7 +30,7 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
gameObject.name = "thisEnemy";
|
||||
}
|
||||
|
||||
// 初始化/复活为指定的 EnemyData_SO
|
||||
// ��ʼ��/����Ϊָ���� EnemyData_SO
|
||||
public void InitializeFromSO(EnemyData_SO so)
|
||||
{
|
||||
sourceData = so;
|
||||
@@ -67,7 +67,7 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
TryTriggerPrimarySkill(SkillDefinition.SkillTrigger.OnEnemyRevive);
|
||||
}
|
||||
|
||||
// 尝试触发 primary skill,当触发条件匹配时调用 SkillBuilder
|
||||
// ���Դ��� primary skill������������ƥ��ʱ���� SkillBuilder
|
||||
private void TryTriggerPrimarySkill(SkillDefinition.SkillTrigger when)
|
||||
{
|
||||
if (sourceData == null) return;
|
||||
@@ -199,7 +199,7 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
else
|
||||
{
|
||||
var sb = FindObjectOfType<SkillBuilder>();
|
||||
var sb = UnityEngine.Object.FindAnyObjectByType<SkillBuilder>();
|
||||
if (sb != null)
|
||||
{
|
||||
Debug.LogWarning("[EnemyCombatant] SkillBuilder.Instance was null on Die(); using FindObjectOfType fallback");
|
||||
@@ -223,7 +223,7 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
{
|
||||
try
|
||||
{
|
||||
var all = GameObject.FindObjectsOfType<EnemyCombatant>(true);
|
||||
var all = UnityEngine.Object.FindObjectsByType<EnemyCombatant>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
bool anyAlive = false;
|
||||
foreach (var e in all)
|
||||
{
|
||||
@@ -244,7 +244,7 @@ public class EnemyCombatant : MonoBehaviour, ICombatant
|
||||
}
|
||||
else
|
||||
{
|
||||
var sb = FindObjectOfType<SkillBuilder>();
|
||||
var sb = UnityEngine.Object.FindAnyObjectByType<SkillBuilder>();
|
||||
if (sb != null)
|
||||
{
|
||||
sb.TriggerOnAllEnemiesDefeated();
|
||||
|
||||
@@ -32,6 +32,17 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
private int[] idolScoreSums = new int[5];
|
||||
|
||||
private readonly int[] tmpCurrents = new int[5];
|
||||
private readonly int[] tmpMaxes = new int[5];
|
||||
|
||||
private readonly GameObject[] allyObjects = new GameObject[5];
|
||||
private readonly AllyCombatant[] allyCombatants = new AllyCombatant[5];
|
||||
private int lastAllyCacheFrame = -9999;
|
||||
|
||||
private readonly TextMeshProUGUI[] slotTmpFallback = new TextMeshProUGUI[5];
|
||||
private readonly Text[] slotLegacyFallback = new Text[5];
|
||||
private readonly bool[] slotFallbackResolved = new bool[5];
|
||||
|
||||
[Header("Judgement Statistics")]
|
||||
public int countPerfect = 0;
|
||||
public int countGreat = 0;
|
||||
@@ -93,6 +104,43 @@ public class ScoreManager : MonoBehaviour
|
||||
ResetStatistics();
|
||||
}
|
||||
|
||||
private void EnsureAllyCache()
|
||||
{
|
||||
int frame = Time.frameCount;
|
||||
if (frame - lastAllyCacheFrame < 30) return;
|
||||
lastAllyCacheFrame = frame;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
if (allyCombatants[i] != null) continue;
|
||||
|
||||
var go = allyObjects[i];
|
||||
if (go == null)
|
||||
{
|
||||
go = GameObject.Find($"ally_0{i + 1}");
|
||||
allyObjects[i] = go;
|
||||
}
|
||||
if (go != null)
|
||||
{
|
||||
allyCombatants[i] = go.GetComponent<AllyCombatant>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveSlotFallbackUi(int slotIndex, GameObject parent)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= 5) return;
|
||||
if (slotFallbackResolved[slotIndex]) return;
|
||||
if (parent == null) return;
|
||||
|
||||
slotTmpFallback[slotIndex] = parent.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (slotTmpFallback[slotIndex] == null)
|
||||
{
|
||||
slotLegacyFallback[slotIndex] = parent.GetComponentInChildren<Text>(true);
|
||||
}
|
||||
slotFallbackResolved[slotIndex] = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a per-note pm score delta to the specified track (0..4) and update named fields + aggregate.
|
||||
/// Also calculates idol score as pmDelta * scoreEfficiency.
|
||||
@@ -133,7 +181,8 @@ public class ScoreManager : MonoBehaviour
|
||||
allSum_pmScore = (int)Mathf.Min((float)aggPm, (float)int.MaxValue - 1f);
|
||||
allSum_idolScore = (int)Mathf.Min((float)aggIdol, (float)int.MaxValue - 1f);
|
||||
|
||||
Debug.Log($"[ScoreManager] Added {pmDelta} to track {trackIndex} pm sum, idol delta {idolDelta}. New pm sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSumPm={allSum_pmScore}. Idol sums: R{red_idolScore_sum} G{green_idolScore_sum} Y{yellow_idolScore_sum} P{purple_idolScore_sum} B{blue_idolScore_sum} -> allSumIdol={allSum_idolScore}");
|
||||
if (JudgeManager.IsDebugEnabled)
|
||||
Debug.Log($"[ScoreManager] Added {pmDelta} to track {trackIndex} pm sum, idol delta {idolDelta}. New pm sums: R{red_pmScore_sum} G{green_pmScore_sum} Y{yellow_pmScore_sum} P{purple_pmScore_sum} B{blue_pmScore_sum} -> allSumPm={allSum_pmScore}. Idol sums: R{red_idolScore_sum} G{green_idolScore_sum} Y{yellow_idolScore_sum} P{purple_idolScore_sum} B{blue_idolScore_sum} -> allSumIdol={allSum_idolScore}");
|
||||
|
||||
// Update UI in teamUIController if available (per-track pm sums + aggregate)
|
||||
var ui = teamUIController.Instance;
|
||||
@@ -158,7 +207,7 @@ public class ScoreManager : MonoBehaviour
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ScoreManager] Failed to write sums to teamUIController fields: {ex}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[ScoreManager] Failed to write sums to teamUIController fields: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,26 +219,20 @@ public class ScoreManager : MonoBehaviour
|
||||
|
||||
public void RecalculateTotal()
|
||||
{
|
||||
int sum = 0;
|
||||
int[] currents = new int[5];
|
||||
int[] maxes = new int[5];
|
||||
for (int i = 1; i <= 5; i++)
|
||||
EnsureAllyCache();
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var go = GameObject.Find($"ally_0{i}");
|
||||
if (go == null) { currents[i - 1] = 0; maxes[i - 1] = 0; continue; }
|
||||
var ally = go.GetComponent<AllyCombatant>();
|
||||
var ally = allyCombatants[i];
|
||||
if (ally != null)
|
||||
{
|
||||
currents[i - 1] = ally.currentScore;
|
||||
maxes[i - 1] = ally.maxTrackScore;
|
||||
tmpCurrents[i] = ally.currentScore;
|
||||
tmpMaxes[i] = ally.maxTrackScore;
|
||||
}
|
||||
else
|
||||
{
|
||||
currents[i - 1] = 0;
|
||||
maxes[i - 1] = 0;
|
||||
tmpCurrents[i] = 0;
|
||||
tmpMaxes[i] = 0;
|
||||
}
|
||||
|
||||
sum += currents[i - 1];
|
||||
}
|
||||
|
||||
// totalScore should include both pm (per-note) and idol aggregates maintained by ScoreManager
|
||||
@@ -209,36 +252,62 @@ public class ScoreManager : MonoBehaviour
|
||||
if (assignedTmp != null)
|
||||
{
|
||||
assignedTmp.text = value;
|
||||
Debug.Log($"[ScoreManager] Wrote to assigned TMP for {slotName}: '{value}' -> {assignedTmp.gameObject.name}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to assigned TMP for {slotName}: '{value}' -> {assignedTmp.gameObject.name}");
|
||||
return;
|
||||
}
|
||||
// try find TMP in parent
|
||||
if (parent != null)
|
||||
{
|
||||
var tmp = parent.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (tmp != null)
|
||||
int idx = slotName == "teammate01" ? 0 :
|
||||
slotName == "teammate02" ? 1 :
|
||||
slotName == "teammate03" ? 2 :
|
||||
slotName == "teammate04" ? 3 :
|
||||
slotName == "teammate05" ? 4 : -1;
|
||||
|
||||
if (idx >= 0)
|
||||
{
|
||||
tmp.text = value;
|
||||
Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
|
||||
return;
|
||||
ResolveSlotFallbackUi(idx, parent);
|
||||
var tmp = slotTmpFallback[idx];
|
||||
if (tmp != null)
|
||||
{
|
||||
tmp.text = value;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
|
||||
return;
|
||||
}
|
||||
var legacy = slotLegacyFallback[idx];
|
||||
if (legacy != null)
|
||||
{
|
||||
legacy.text = value;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
var legacy = parent.GetComponentInChildren<Text>(true);
|
||||
if (legacy != null)
|
||||
else
|
||||
{
|
||||
legacy.text = value;
|
||||
Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
|
||||
return;
|
||||
var tmp = parent.GetComponentInChildren<TextMeshProUGUI>(true);
|
||||
if (tmp != null)
|
||||
{
|
||||
tmp.text = value;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to TMP in parent {parent.name} for {slotName}: '{value}' -> {tmp.gameObject.name}");
|
||||
return;
|
||||
}
|
||||
var legacy = parent.GetComponentInChildren<Text>(true);
|
||||
if (legacy != null)
|
||||
{
|
||||
legacy.text = value;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote to legacy Text in parent {parent.name} for {slotName}: '{value}' -> {legacy.gameObject.name}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Debug.LogWarning($"[ScoreManager] Could not find UI to write score for {slotName}. assignedTmp is null and parent '{parent?.name}' has no TMP/Text children.");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[ScoreManager] Could not find UI to write score for {slotName}. assignedTmp is null and parent '{parent?.name}' has no TMP/Text children.");
|
||||
}
|
||||
|
||||
// Update each slot: use the explicit TMP fields if set; otherwise try parent object fields
|
||||
SetScoreText(ui.teammate01_current_scoreText, ui.objectFather_ally01, currents[0], maxes[0], "teammate01");
|
||||
SetScoreText(ui.teammate02_current_scoreText, ui.objectFather_ally02, currents[1], maxes[1], "teammate02");
|
||||
SetScoreText(ui.teammate03_current_scoreText, ui.objectFather_ally03, currents[2], maxes[2], "teammate03");
|
||||
SetScoreText(ui.teammate04_current_scoreText, ui.objectFather_ally04, currents[3], maxes[3], "teammate04");
|
||||
SetScoreText(ui.teammate05_current_scoreText, ui.objectFather_ally05, currents[4], maxes[4], "teammate05");
|
||||
SetScoreText(ui.teammate01_current_scoreText, ui.objectFather_ally01, tmpCurrents[0], tmpMaxes[0], "teammate01");
|
||||
SetScoreText(ui.teammate02_current_scoreText, ui.objectFather_ally02, tmpCurrents[1], tmpMaxes[1], "teammate02");
|
||||
SetScoreText(ui.teammate03_current_scoreText, ui.objectFather_ally03, tmpCurrents[2], tmpMaxes[2], "teammate03");
|
||||
SetScoreText(ui.teammate04_current_scoreText, ui.objectFather_ally04, tmpCurrents[3], tmpMaxes[3], "teammate04");
|
||||
SetScoreText(ui.teammate05_current_scoreText, ui.objectFather_ally05, tmpCurrents[4], tmpMaxes[4], "teammate05");
|
||||
|
||||
// update total score (try TMP first, then legacy Text)
|
||||
if (ui.currentTotalScore != null)
|
||||
@@ -250,7 +319,7 @@ public class ScoreManager : MonoBehaviour
|
||||
if (tmpComp != null)
|
||||
{
|
||||
tmpComp.text = totalScore.ToString();
|
||||
Debug.Log($"[ScoreManager] Wrote total to TMP: {totalScore} -> {tmpComp.gameObject.name}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to TMP: {totalScore} -> {tmpComp.gameObject.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -258,29 +327,29 @@ public class ScoreManager : MonoBehaviour
|
||||
if (legacyComp != null)
|
||||
{
|
||||
legacyComp.text = totalScore.ToString();
|
||||
Debug.Log($"[ScoreManager] Wrote total to legacy Text: {totalScore} -> {legacyComp.gameObject.name}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to legacy Text: {totalScore} -> {legacyComp.gameObject.name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
ui.currentTotalScore.text = totalScore.ToString(); // fallback
|
||||
Debug.Log($"[ScoreManager] Wrote total to currentTotalScore field fallback: {totalScore}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to currentTotalScore field fallback: {totalScore}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ui.currentTotalScore.text = totalScore.ToString();
|
||||
Debug.Log($"[ScoreManager] Wrote total to currentTotalScore fallback (no go): {totalScore}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[ScoreManager] Wrote total to currentTotalScore fallback (no go): {totalScore}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[ScoreManager] teamUIController.currentTotalScore is null; cannot display total score.");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("[ScoreManager] teamUIController.currentTotalScore is null; cannot display total score.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("[ScoreManager] teamUIController.Instance is null; cannot update UI.");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning("[ScoreManager] teamUIController.Instance is null; cannot update UI.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// SkillBuilder: ���ж���ɿ��ٵ��õļ��ܺ�����
|
||||
/// SkillBuilder: ���ж���ɿ��ٵ��õļ��ܺ�����?
|
||||
/// - ÿ������Ϊһ�������� public ���������ܱ�Ҫ������ʩ���� source, Ŀ�� specificTarget �ȣ�
|
||||
/// - �ڲ����� EffectSystem.Instance.ApplyEffect(...)��ͳһʹ�� Selector �� EffectType
|
||||
/// - �ṩ���ò�����amount, duration, tickInterval�����Բ�������У��
|
||||
///
|
||||
/// �÷�ʾ��:
|
||||
/// SkillBuilder.Instance.ExecuteSkill("Fireball", EffectType.DamageSingleEnemy, 120f, Selector.CurrentEnemies, caster, target);
|
||||
/// SkillBuilder.Instance.ApplyScoreMultiplier(caster, Selector.AllAllies, 1.5f, 5f); // 5 ���ڶ���÷ֳ� 1.5
|
||||
/// SkillBuilder.Instance.ApplyScoreMultiplier(caster, Selector.AllAllies, 1.5f, 5f); // 5 ���ڶ���÷ֳ�?1.5
|
||||
/// </summary>
|
||||
public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
@@ -64,6 +64,12 @@ public class SkillBuilder : MonoBehaviour
|
||||
private Dictionary<int, AllyHero_SO> _allyHeroSoById;
|
||||
private Dictionary<int, AllyHero_SO> _allyHeroSoBySlotCache;
|
||||
|
||||
// Reusable buffers to reduce allocations during gameplay
|
||||
private readonly Dictionary<string, float> _varsBuffer = new Dictionary<string, float>(16);
|
||||
private readonly List<string> _tmpNoteIdRemoval = new List<string>(16);
|
||||
private int _cachedAlliesFrame = -1;
|
||||
private readonly List<GameObject> _cachedAllies = new List<GameObject>(8);
|
||||
|
||||
private void PrewarmAllyHeroSOIndex()
|
||||
{
|
||||
if (_allAllyHeroSOs != null && _allAllyHeroSOs.Length > 0) return;
|
||||
@@ -85,7 +91,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
|
||||
if (GameConfig.verboseLogs)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] PrewarmAllyHeroSOIndex: loaded {_allAllyHeroSOs?.Length ?? 0} AllyHero_SO assets, indexed {_allyHeroSoById.Count}");
|
||||
LogVerbose($"[SkillBuilder] PrewarmAllyHeroSOIndex: loaded {_allAllyHeroSOs?.Length ?? 0} AllyHero_SO assets, indexed {_allyHeroSoById.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +107,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
// Apply as single-instance damage to current enemies; EffectSystem will pick first applicable enemy for DamageSingleEnemy
|
||||
global::EffectSystem.Instance.ApplyEffect(Selector.CurrentEnemies, EffectType.DamageSingleEnemy, amount, 0f, caster, null);
|
||||
Debug.Log($"[SkillBuilder] DealDamageFromAllyToEnemies: applied {amount} from slot {slotIndex} via EffectSystem");
|
||||
LogVerbose($"[SkillBuilder] DealDamageFromAllyToEnemies: applied {amount} from slot {slotIndex} via EffectSystem");
|
||||
}
|
||||
|
||||
private void ApplySharedOnNoteHit(int trackIndex, string judgeResult)
|
||||
@@ -140,7 +146,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (ally != null && manaGain != 0)
|
||||
{
|
||||
ally.ModifyMana(manaGain, true, false);
|
||||
Debug.Log($"[SkillBuilder] Applied mana gain {manaGain} to slot {trackIndex} due to judge {judgeResult}");
|
||||
LogVerbose($"[SkillBuilder] Applied mana gain {manaGain} to slot {trackIndex} due to judge {judgeResult}");
|
||||
}
|
||||
|
||||
if (judgeResult == "Miss")
|
||||
@@ -151,7 +157,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
// apply damage reduction by damageResistance: final loss = missBase * (1 - damageResistance)
|
||||
float loss = missBase * (1f - ally.damageResistance);
|
||||
ally.ModifyHP(-Mathf.CeilToInt(loss), true);
|
||||
Debug.Log($"[SkillBuilder] Applied miss HP loss {loss} to slot {trackIndex} (damageResistance={ally.damageResistance})");
|
||||
LogVerbose($"[SkillBuilder] Applied miss HP loss {loss} to slot {trackIndex} (damageResistance={ally.damageResistance})");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -167,7 +173,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
|
||||
// ----------------------------- ͨ�ü���ִ�нӿ� -----------------------------
|
||||
// ͨ��ִ�У�������, Ч������, ��ֵ, Ŀ��ѡ����, ʩ����, ��ѡ����Ŀ��, ����ʱ��� tick
|
||||
// ͨ��ִ�У�������, Ч������, ��ֵ, Ŀ��ѡ����, ʩ����, ��ѡ����Ŀ��, ����ʱ���?tick
|
||||
public void ExecuteSkill(string skillName, EffectType effectType, float amount, Selector selector, GameObject caster, GameObject specificTarget = null, float duration = 0f, float tickInterval = 1f)
|
||||
{
|
||||
if (global::EffectSystem.Instance == null)
|
||||
@@ -176,7 +182,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
// basic logging
|
||||
Debug.Log($"[SkillBuilder] ExecuteSkill: {skillName} type={effectType} amount={amount} selector={selector} caster={(caster?caster.name:"null")} target={(specificTarget?specificTarget.name:"null")} duration={duration}");
|
||||
LogVerbose($"[SkillBuilder] ExecuteSkill: {skillName} type={effectType} amount={amount} selector={selector} caster={(caster?caster.name:"null")} target={(specificTarget?specificTarget.name:"null")} duration={duration}");
|
||||
|
||||
// additional debug: show when caster is null which will make Selector.Self produce no targets
|
||||
if (selector == Selector.Self && caster == null)
|
||||
@@ -196,7 +202,51 @@ public class SkillBuilder : MonoBehaviour
|
||||
ExecuteSkill(skillName, effectType, amount, selector, casterGO, targetGO, duration, tickInterval);
|
||||
}
|
||||
|
||||
// ----------------------------- Ŀ������������ű�ʹ�ã��� EffectSystem �Ľ���һ�£� -----------------------------
|
||||
|
||||
private void LogVerbose(string message)
|
||||
{
|
||||
if (GameConfig.verboseLogs) Debug.Log(message);
|
||||
}
|
||||
|
||||
private List<GameObject> GetAlliesCached()
|
||||
{
|
||||
if (_cachedAlliesFrame == Time.frameCount) return _cachedAllies;
|
||||
_cachedAlliesFrame = Time.frameCount;
|
||||
_cachedAllies.Clear();
|
||||
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null)
|
||||
{
|
||||
int count = ui.allySlotIds != null ? ui.allySlotIds.Count : 5;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var go = ui.GetAllyObjectBySlot(i);
|
||||
if (go != null && !_cachedAllies.Contains(go)) _cachedAllies.Add(go);
|
||||
}
|
||||
}
|
||||
|
||||
if (_cachedAllies.Count == 0)
|
||||
{
|
||||
for (int i = 1; i <= 5; i++)
|
||||
{
|
||||
var go = GameObject.Find($"ally_0{i}");
|
||||
if (go != null && !_cachedAllies.Contains(go)) _cachedAllies.Add(go);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var tagged = GameObject.FindGameObjectsWithTag("Ally");
|
||||
foreach (var g in tagged)
|
||||
{
|
||||
if (g != null && !_cachedAllies.Contains(g)) _cachedAllies.Add(g);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return _cachedAllies;
|
||||
}
|
||||
// ----------------------------- Ŀ������������ű�ʹ�ã���?EffectSystem �Ľ���һ�£� -----------------------------
|
||||
// ���ط��� selector �� GameObject �б������㼼�ܽű����в���������ֱ�Ӹ� ICombatant.Buff��
|
||||
public List<GameObject> ResolveTargetsLocal(Selector selector, GameObject source = null, GameObject specificTarget = null)
|
||||
{
|
||||
@@ -213,12 +263,16 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (source != null) list.Add(source);
|
||||
break;
|
||||
case Selector.AllAllies:
|
||||
for (int i = 1; i <= 5; i++) { var go = GameObject.Find($"ally_0{i}"); if (go != null) list.Add(go); }
|
||||
try { var tagged = GameObject.FindGameObjectsWithTag("Ally"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { }
|
||||
foreach (var go in GetAlliesCached())
|
||||
{
|
||||
if (go != null) list.Add(go);
|
||||
}
|
||||
break;
|
||||
case Selector.AllAlliesExceptSelf:
|
||||
for (int i = 1; i <= 5; i++) { var go = GameObject.Find($"ally_0{i}"); if (go != null) list.Add(go); }
|
||||
try { var tagged = GameObject.FindGameObjectsWithTag("Ally"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { }
|
||||
foreach (var go in GetAlliesCached())
|
||||
{
|
||||
if (go != null) list.Add(go);
|
||||
}
|
||||
if (source != null) list.RemoveAll(g => g == null || g == source);
|
||||
break;
|
||||
case Selector.AdjacentAllies:
|
||||
@@ -275,8 +329,10 @@ public class SkillBuilder : MonoBehaviour
|
||||
else { try { var tagged = GameObject.FindGameObjectsWithTag("Enemy"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { var single = GameObject.Find("thisEnemy"); if (single != null) list.Add(single); } }
|
||||
break;
|
||||
case Selector.AllEntities:
|
||||
for (int i = 1; i <= 5; i++) { var go = GameObject.Find($"ally_0{i}"); if (go != null) list.Add(go); }
|
||||
try { var tagged = GameObject.FindGameObjectsWithTag("Ally"); foreach (var g in tagged) if (!list.Contains(g)) list.Add(g); } catch { }
|
||||
foreach (var go in GetAlliesCached())
|
||||
{
|
||||
if (go != null) list.Add(go);
|
||||
}
|
||||
try { var taggedE = GameObject.FindGameObjectsWithTag("Enemy"); foreach (var g in taggedE) if (!list.Contains(g)) list.Add(g); } catch { var single = GameObject.Find("thisEnemy"); if (single != null && !list.Contains(single)) list.Add(single); }
|
||||
break;
|
||||
}
|
||||
@@ -379,7 +435,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- ���б�ݼ���ʾ�� -----------------------------
|
||||
// ----------------------------- ���б�ݼ���ʾ��?-----------------------------
|
||||
public void DealSingleEnemyDamage(GameObject caster, GameObject enemyTarget, float amount)
|
||||
{
|
||||
if (enemyTarget == null) { Debug.LogWarning("DealSingleEnemyDamage: enemyTarget == null"); return; }
|
||||
@@ -549,7 +605,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
return best;
|
||||
}
|
||||
|
||||
// ȡ��Ӣ�ۻ�����������ʹ�õ�ǰ����ƥ��ĵȼ��� attack������û���� 0
|
||||
// ȡ��Ӣ�ۻ�����������ʹ�õ�ǰ����ƥ��ĵȼ���?attack������û���� 0
|
||||
public int GetAllyBaseAttack(AllyHero_SO so)
|
||||
{
|
||||
if (so == null) return 0;
|
||||
@@ -559,8 +615,8 @@ public class SkillBuilder : MonoBehaviour
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ͳһ���㼼�������ֵ���� inputValue == -1 ʱʹ�� skillStatic + allyAttack��
|
||||
// ����ֱ��ʹ�� inputValue��������� EffectType ����Ҫת����
|
||||
// ͳһ���㼼�������ֵ����?inputValue == -1 ʱʹ�� skillStatic + allyAttack��
|
||||
// ����ֱ��ʹ�� inputValue���������?EffectType ����Ҫת����
|
||||
public float ComputeSkillValue(GameObject caster, int slotIndex, float inputValue, int skillStatic)
|
||||
{
|
||||
if (inputValue != -1f) return inputValue;
|
||||
@@ -619,7 +675,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (alt != null)
|
||||
{
|
||||
caster = alt;
|
||||
Debug.Log($"UseSkillDefinition: resolved caster via teamUIController for slot {slotIndex} -> {caster.name}");
|
||||
LogVerbose($"UseSkillDefinition: resolved caster via teamUIController for slot {slotIndex} -> {caster.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -631,12 +687,12 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (byName != null)
|
||||
{
|
||||
caster = byName;
|
||||
Debug.Log($"UseSkillDefinition: resolved caster via name ally_0{slotIndex + 1} -> {caster.name}");
|
||||
LogVerbose($"UseSkillDefinition: resolved caster via name ally_0{slotIndex + 1} -> {caster.name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Log useful debug info for diagnosing OnEnemyDead->Self issues
|
||||
Debug.Log($"[SkillBuilder] UseSkillDefinition: casting skill {def.skillId} for slot {slotIndex} (caster={(caster?caster.name:"null")}) selector={def.defaultSelector} operateDirectly={def.operateDirectly} inputValue={inputValue} specificTarget={(specificTarget?specificTarget.name:"null")}");
|
||||
LogVerbose($"[SkillBuilder] UseSkillDefinition: casting skill {def.skillId} for slot {slotIndex} (caster={(caster?caster.name:"null")}) selector={def.defaultSelector} operateDirectly={def.operateDirectly} inputValue={inputValue} specificTarget={(specificTarget?specificTarget.name:"null")}");
|
||||
|
||||
float amount = 0f;
|
||||
// If caller provided explicit inputValue use it
|
||||
@@ -648,7 +704,9 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
// Build variables from SO and caster; formula can be a plain number too
|
||||
var so = GetAllyHeroSOBySlot(slotIndex);
|
||||
var vars = new Dictionary<string, float>();
|
||||
var casterAlly = caster != null ? caster.GetComponent<AllyCombatant>() : null;
|
||||
var vars = _varsBuffer;
|
||||
vars.Clear();
|
||||
vars["slot"] = slotIndex;
|
||||
vars["attack"] = GetAllyBaseAttack(so);
|
||||
if (so != null && so.levelStats != null && so.levelStats.Count > 0)
|
||||
@@ -660,6 +718,9 @@ public class SkillBuilder : MonoBehaviour
|
||||
vars["damageResistance"] = lvl.damageResistance;
|
||||
vars["scoreEfficiency"] = lvl.scoreEfficiency;
|
||||
vars["attack"] = lvl.attack; // ensure attack from SO levelStats is available
|
||||
vars["level"] = lvl.levelID;
|
||||
vars["levelID"] = lvl.levelID;
|
||||
vars["currentLevel"] = lvl.levelID;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -667,9 +728,16 @@ public class SkillBuilder : MonoBehaviour
|
||||
vars["maxMana"] = 0f;
|
||||
vars["damageResistance"] = 0f;
|
||||
vars["scoreEfficiency"] = 1f;
|
||||
vars["level"] = 0f;
|
||||
vars["levelID"] = 0f;
|
||||
vars["currentLevel"] = 0f;
|
||||
}
|
||||
if (so != null) vars["ally_currentEXP"] = so.ally_currentEXP;
|
||||
else vars["ally_currentEXP"] = 0f;
|
||||
vars["currentMana"] = casterAlly != null ? casterAlly.currentMana : 0f;
|
||||
vars["currentScore"] = casterAlly != null ? casterAlly.currentScore : 0f;
|
||||
vars["idolScore"] = vars["currentScore"];
|
||||
vars["currentHP"] = casterAlly != null ? casterAlly.currentHP : 0f;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(def.formula))
|
||||
{
|
||||
@@ -723,6 +791,12 @@ public class SkillBuilder : MonoBehaviour
|
||||
foreach (var t in targets)
|
||||
{
|
||||
if (t == null) continue;
|
||||
|
||||
if (GameConfig.skillDebugMode)
|
||||
{
|
||||
Debug.Log($"<color=#00FFFF>[SkillDebug]</color> <color=#FFD700>ID: {def.skillId}</color> | <color=#00FF00>Trigger: {def.triggerCondition}</color> | <color=#FFA500>Target: {t.name}</color> | <color=#EE82EE>Effect: {def.effectType}</color> | <color=#FF4500>Value: {amountTotal:F2}</color>");
|
||||
}
|
||||
|
||||
var ally = t.GetComponent<AllyCombatant>();
|
||||
var ic = t.GetComponent<ICombatant>();
|
||||
|
||||
@@ -811,13 +885,16 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (ally != null) ally.scoreEfficiency = Mathf.Max(0f, ally.scoreEfficiency - amountTotal);
|
||||
break;
|
||||
case EffectType.IncreaseAttack:
|
||||
if (ally != null) ally.attack += Mathf.CeilToInt(amountTotal);
|
||||
if (ally != null) ally.ModifyAttack(Mathf.CeilToInt(amountTotal));
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy5)) enemy5.attack += Mathf.CeilToInt(amountTotal);
|
||||
break;
|
||||
case EffectType.DecreaseAttack:
|
||||
if (ally != null) ally.attack = Mathf.Max(0, ally.attack - Mathf.CeilToInt(amountTotal));
|
||||
if (ally != null) ally.ModifyAttack(-Mathf.CeilToInt(amountTotal));
|
||||
else if (t.TryGetComponent<EnemyCombatant>(out var enemy6)) enemy6.attack = Mathf.Max(0, enemy6.attack - Mathf.CeilToInt(amountTotal));
|
||||
break;
|
||||
case EffectType.RedirectNextDamageToSelf:
|
||||
if (ally != null) AllyCombatant.ActivateNextDamageRedirect(ally, def.defaultDuration);
|
||||
break;
|
||||
default:
|
||||
// non-direct path handled below, but keep compatibility
|
||||
ExecuteSkill(def.displayName ?? def.skillId, def.effectType, amountTotal, def.defaultSelector, caster, specificTarget, def.defaultDuration, def.defaultTickInterval);
|
||||
@@ -827,6 +904,10 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameConfig.skillDebugMode)
|
||||
{
|
||||
Debug.Log($"<color=#00FFFF>[SkillDebug]</color> <color=#FFD700>ID: {def.skillId}</color> | <color=#00FF00>Trigger: {def.triggerCondition}</color> | <color=#FFA500>Target: (Resolving via EffectSystem)</color> | <color=#EE82EE>Effect: {def.effectType}</color> | <color=#FF4500>Value: {amountTotal:F2}</color>");
|
||||
}
|
||||
// For non-direct path, EffectSystem expects 'amount' to be total amount for sustained effects
|
||||
ExecuteSkill(def.displayName ?? def.skillId, def.effectType, amountTotal, def.defaultSelector, caster, specificTarget, def.defaultDuration, def.defaultTickInterval);
|
||||
}
|
||||
@@ -895,7 +976,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
var s = group.skills[i];
|
||||
if (s == null) continue;
|
||||
UseSkillDefinition(s, slotIndex, inputValue, specificTarget);
|
||||
Debug.Log($"[SkillBuilder] UseSkillGroupForSlot: cast skill {s.skillId} from group '{group.groupName}' for slot {slotIndex}");
|
||||
LogVerbose($"[SkillBuilder] UseSkillGroupForSlot: cast skill {s.skillId} from group '{group.groupName}' for slot {slotIndex}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,7 +1004,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (sk == null) continue;
|
||||
if (sk.triggerCondition == SkillDefinition.SkillTrigger.OnGameStart)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] TriggerOnGameStart: casting equipped group skill {sk.skillId} for slot {i}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnGameStart: casting equipped group skill {sk.skillId} for slot {i}");
|
||||
UseSkillDefinition(sk, i, -1f, null);
|
||||
}
|
||||
}
|
||||
@@ -940,7 +1021,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (sk == null) continue;
|
||||
if (sk.triggerCondition == SkillDefinition.SkillTrigger.OnGameStart)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] TriggerOnGameStart: casting group skill {sk.skillId} for slot {i}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnGameStart: casting group skill {sk.skillId} for slot {i}");
|
||||
UseSkillDefinition(sk, i, -1f, null);
|
||||
}
|
||||
}
|
||||
@@ -951,7 +1032,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition == SkillDefinition.SkillTrigger.OnGameStart)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] TriggerOnGameStart: casting primary skill {def.skillId} for slot {i}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnGameStart: casting primary skill {def.skillId} for slot {i}");
|
||||
UsePrimarySkillForSlot(i, -1f, null);
|
||||
}
|
||||
}
|
||||
@@ -985,7 +1066,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnEnemyDead) continue;
|
||||
Debug.Log($"[SkillBuilder] TriggerOnEnemyDead: slot {i+1} casting skill {def.skillId}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnEnemyDead: slot {i+1} casting skill {def.skillId}");
|
||||
// Only pass deadEnemy as specificTarget when the skill truly expects a specific target (e.g. targets CurrentEnemies)
|
||||
GameObject ctxTarget = null;
|
||||
if (deadEnemy != null && (def.requiresSpecificTarget || def.defaultSelector == Selector.CurrentEnemies)) ctxTarget = deadEnemy.gameObject;
|
||||
@@ -1002,7 +1083,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnEnemyDead) continue;
|
||||
Debug.Log($"[SkillBuilder] TriggerOnEnemyDead: slot {i+1} casting skill {def.skillId}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnEnemyDead: slot {i+1} casting skill {def.skillId}");
|
||||
GameObject ctxTarget = null;
|
||||
if (deadEnemy != null && (def.requiresSpecificTarget || def.defaultSelector == Selector.CurrentEnemies)) ctxTarget = deadEnemy.gameObject;
|
||||
UseSkillDefinition(def, i, -1f, ctxTarget);
|
||||
@@ -1013,7 +1094,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
var primary = so.GetPrimarySkill();
|
||||
if (primary != null && primary.triggerCondition == SkillDefinition.SkillTrigger.OnEnemyDead)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] TriggerOnEnemyDead: slot {i+1} casting primary skill {primary.skillId}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnEnemyDead: slot {i+1} casting primary skill {primary.skillId}");
|
||||
// Only pass deadEnemy as specificTarget when the skill truly expects a specific target (e.g. targets CurrentEnemies)
|
||||
GameObject ctxTarget = null;
|
||||
if (deadEnemy != null && (primary.requiresSpecificTarget || primary.defaultSelector == Selector.CurrentEnemies)) ctxTarget = deadEnemy.gameObject;
|
||||
@@ -1049,7 +1130,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnEnemyRevive) continue;
|
||||
Debug.Log($"[SkillBuilder] TriggerOnEnemyRevive: slot {i+1} casting skill {def.skillId}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnEnemyRevive: slot {i+1} casting skill {def.skillId}");
|
||||
UseSkillDefinition(def, i, -1f, enemy != null ? enemy.gameObject : null);
|
||||
}
|
||||
}
|
||||
@@ -1063,7 +1144,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
if (def == null) continue;
|
||||
if (def.triggerCondition != SkillDefinition.SkillTrigger.OnEnemyRevive) continue;
|
||||
Debug.Log($"[SkillBuilder] TriggerOnEnemyRevive: slot {i+1} casting skill {def.skillId} (from primary group)");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnEnemyRevive: slot {i+1} casting skill {def.skillId} (from primary group)");
|
||||
UseSkillDefinition(def, i, -1f, enemy != null ? enemy.gameObject : null);
|
||||
}
|
||||
continue;
|
||||
@@ -1072,7 +1153,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
var primary2 = so.GetPrimarySkill();
|
||||
if (primary2 != null && primary2.triggerCondition == SkillDefinition.SkillTrigger.OnEnemyRevive)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] TriggerOnEnemyRevive: slot {i+1} casting primary skill {primary2.skillId}");
|
||||
LogVerbose($"[SkillBuilder] TriggerOnEnemyRevive: slot {i+1} casting primary skill {primary2.skillId}");
|
||||
UsePrimarySkillForSlot(i, -1f, enemy != null ? enemy.gameObject : null);
|
||||
}
|
||||
}
|
||||
@@ -1137,10 +1218,11 @@ public class SkillBuilder : MonoBehaviour
|
||||
// noteType indicates whether the hit event came from a Tap or Hold (tail)
|
||||
public bool NotifyNoteHit(int trackIndex, string judgeResult, SkillDefinition.NoteTypeTrigger noteType = SkillDefinition.NoteTypeTrigger.Tap, string uniqueNoteId = null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit RECEIVED: track={trackIndex} judge={judgeResult} noteType={noteType} uniqueId={uniqueNoteId}");
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit RECEIVED: track={trackIndex} judge={judgeResult} noteType={noteType} uniqueId={uniqueNoteId}");
|
||||
// cleanup old processed ids (> 10s)
|
||||
float nowCleanup = Time.time;
|
||||
var toRemove = new List<string>();
|
||||
_tmpNoteIdRemoval.Clear();
|
||||
var toRemove = _tmpNoteIdRemoval;
|
||||
foreach (var kv in _processedNoteHitTimestamps)
|
||||
{
|
||||
if (nowCleanup - kv.Value > 10f) toRemove.Add(kv.Key);
|
||||
@@ -1167,21 +1249,21 @@ public class SkillBuilder : MonoBehaviour
|
||||
try { ApplySharedOnNoteHit(trackIndex, judgeResult); } catch (System.Exception ex) { Debug.LogError($"[SkillBuilder] ApplySharedOnNoteHit threw: {ex}"); }
|
||||
}
|
||||
|
||||
if (trackIndex < 0) { Debug.Log("[SkillBuilder] NotifyNoteHit: invalid trackIndex"); return false; }
|
||||
if (trackIndex < 0) { LogVerbose("[SkillBuilder] NotifyNoteHit: invalid trackIndex"); return false; }
|
||||
var so = GetAllyHeroSOBySlot(trackIndex);
|
||||
if (so == null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: no SO found for slot {trackIndex}, attempting fallback resolution...");
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit: no SO found for slot {trackIndex}, attempting fallback resolution...");
|
||||
// Dump current UI slot ids for diagnostics
|
||||
if (teamUIController.Instance != null && teamUIController.Instance.allySlotIds != null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] teamUIController.allySlotIds = [{string.Join(",", teamUIController.Instance.allySlotIds)}]");
|
||||
LogVerbose($"[SkillBuilder] teamUIController.allySlotIds = [{string.Join(",", teamUIController.Instance.allySlotIds)}]");
|
||||
}
|
||||
// Try to find ally GameObject named by convention
|
||||
var foundGO = GameObject.Find($"ally_0{trackIndex + 1}");
|
||||
if (foundGO != null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] Found GameObject by name ally_0{trackIndex + 1}: {foundGO.name}");
|
||||
LogVerbose($"[SkillBuilder] Found GameObject by name ally_0{trackIndex + 1}: {foundGO.name}");
|
||||
// try to map to UI slot
|
||||
var ui = teamUIController.Instance;
|
||||
if (ui != null && ui.allySlotIds != null)
|
||||
@@ -1192,7 +1274,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
if (slotObj == null) continue;
|
||||
if (slotObj == foundGO || foundGO.transform.IsChildOf(slotObj.transform))
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] Resolved fallback slot {i} for GameObject {foundGO.name}");
|
||||
LogVerbose($"[SkillBuilder] Resolved fallback slot {i} for GameObject {foundGO.name}");
|
||||
trackIndex = i; // override
|
||||
so = GetAllyHeroSOBySlot(trackIndex);
|
||||
break;
|
||||
@@ -1202,7 +1284,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
if (so == null)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: still no SO for resolved slot {trackIndex}, aborting NotifyNoteHit");
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit: still no SO for resolved slot {trackIndex}, aborting NotifyNoteHit");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1252,7 +1334,7 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
|
||||
// trigger
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: triggering equipped skill {def.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit: triggering equipped skill {def.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
|
||||
UseSkillDefinition(def, trackIndex, -1f, null);
|
||||
_lastOnNoteHitTriggerTime[key] = now;
|
||||
anyTriggered = true;
|
||||
@@ -1263,10 +1345,10 @@ public class SkillBuilder : MonoBehaviour
|
||||
|
||||
// Fallback to previous behavior using primary skill (availableSkills)
|
||||
var defPrimary = so.GetPrimarySkill();
|
||||
if (defPrimary == null) { Debug.Log($"[SkillBuilder] NotifyNoteHit: no primary skill for slot {trackIndex}"); return false; }
|
||||
if (defPrimary == null) { LogVerbose($"[SkillBuilder] NotifyNoteHit: no primary skill for slot {trackIndex}"); return false; }
|
||||
if (defPrimary.triggerCondition != SkillDefinition.SkillTrigger.OnNoteHit)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: primary skill {defPrimary.skillId} for slot {trackIndex} is not set to OnNoteHit (actual={defPrimary.triggerCondition}), rejecting");
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit: primary skill {defPrimary.skillId} for slot {trackIndex} is not set to OnNoteHit (actual={defPrimary.triggerCondition}), rejecting");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1276,10 +1358,10 @@ public class SkillBuilder : MonoBehaviour
|
||||
switch (defPrimary.noteTriggerType)
|
||||
{
|
||||
case SkillDefinition.NoteTypeTrigger.Tap:
|
||||
if (effectiveNoteTypePrimary != SkillDefinition.NoteTypeTrigger.Tap) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires Tap but event is {noteType}, rejecting"); return false; }
|
||||
if (effectiveNoteTypePrimary != SkillDefinition.NoteTypeTrigger.Tap) { LogVerbose($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires Tap but event is {noteType}, rejecting"); return false; }
|
||||
break;
|
||||
case SkillDefinition.NoteTypeTrigger.Hold:
|
||||
if (noteType != SkillDefinition.NoteTypeTrigger.Hold) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires Hold but event is {noteType}, rejecting"); return false; }
|
||||
if (noteType != SkillDefinition.NoteTypeTrigger.Hold) { LogVerbose($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires Hold but event is {noteType}, rejecting"); return false; }
|
||||
break;
|
||||
case SkillDefinition.NoteTypeTrigger.Either:
|
||||
break;
|
||||
@@ -1288,12 +1370,12 @@ public class SkillBuilder : MonoBehaviour
|
||||
int qualityPrimary = JudgeQualityFromString(judgeResult);
|
||||
if (defPrimary.onNoteHitMinThreshold == SkillDefinition.NoteTriggerThreshold.Miss)
|
||||
{
|
||||
if (qualityPrimary != 0) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} is Miss-only but quality={qualityPrimary}, rejecting"); return false; }
|
||||
if (qualityPrimary != 0) { LogVerbose($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} is Miss-only but quality={qualityPrimary}, rejecting"); return false; }
|
||||
}
|
||||
else
|
||||
{
|
||||
int required = (int)defPrimary.onNoteHitMinThreshold;
|
||||
if (qualityPrimary < required) { Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires quality>={required} but got {qualityPrimary}, rejecting"); return false; }
|
||||
if (qualityPrimary < required) { LogVerbose($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} requires quality>={required} but got {qualityPrimary}, rejecting"); return false; }
|
||||
}
|
||||
|
||||
string keyPrimary = $"{trackIndex}:{defPrimary.skillId}";
|
||||
@@ -1302,12 +1384,12 @@ public class SkillBuilder : MonoBehaviour
|
||||
{
|
||||
if (nowPrimary - lastPrimary < defPrimary.onNoteHitCooldown)
|
||||
{
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} on cooldown for slot {trackIndex}");
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit: skill {defPrimary.skillId} on cooldown for slot {trackIndex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[SkillBuilder] NotifyNoteHit: triggering primary skill {defPrimary.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
|
||||
LogVerbose($"[SkillBuilder] NotifyNoteHit: triggering primary skill {defPrimary.skillId} for slot {trackIndex} due to note hit {judgeResult} (noteType={noteType})");
|
||||
UsePrimarySkillForSlot(trackIndex, -1f, null);
|
||||
_lastOnNoteHitTriggerTime[keyPrimary] = nowPrimary;
|
||||
return true;
|
||||
@@ -1323,8 +1405,8 @@ public class SkillBuilder : MonoBehaviour
|
||||
- EnemyTarget: if your skill should hit a specific enemy instance, supply that enemy's GameObject (for example from Enemy spawn manager or from collision detection). If you want to apply to all enemies, pass null and use Selector.CurrentEnemies.
|
||||
*/
|
||||
|
||||
// ----------------------------- ʾ����Ϊ���3 ��װһ����ݺ������û�ʾ���� -----------------------------
|
||||
// ���3(����2) �ļ���1����ѭ�û�����ǩ����_ally03skill01(EffectType, value, Selector, duration)
|
||||
// ----------------------------- ʾ����Ϊ���? ��װһ����ݺ������û�ʾ����?-----------------------------
|
||||
// ���?(����2) �ļ���1����ѭ�û�����ǩ����_ally03skill01(EffectType, value, Selector, duration)
|
||||
public void _ally03skill01(EffectType effectType, float value, Selector selector, float duration = 0f, GameObject specificTarget = null)
|
||||
{
|
||||
AllySlotSkill(2, "skill01", effectType, value, selector, duration, specificTarget);
|
||||
@@ -1423,3 +1505,13 @@ public class SkillBuilder : MonoBehaviour
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,96 +6,100 @@ using System.Globalization;
|
||||
[CreateAssetMenu(fileName = "NewSkillDefinition", menuName = "Combat/SkillDefinition")]
|
||||
public class SkillDefinition : ScriptableObject
|
||||
{
|
||||
// Cache compiled RPN for formulas to avoid re-tokenizing every trigger.
|
||||
private static readonly Dictionary<string, List<Token>> s_rpnCache = new Dictionary<string, List<Token>>();
|
||||
private static readonly HashSet<string> s_badFormulaCache = new HashSet<string>();
|
||||
public enum SkillTrigger
|
||||
{
|
||||
None,
|
||||
OnGameStart,
|
||||
OnNoteHit,
|
||||
OnManaFull,
|
||||
OnEnemyRevive, // 新增:当敌人登场时触发
|
||||
OnEnemyDead, // 新增:当敌人死亡时触发
|
||||
// 新增:当所有敌人被击败时触发
|
||||
OnAttackZero,
|
||||
OnEnemyRevive, // �����������˵dz�ʱ����
|
||||
OnEnemyDead, // ����������������ʱ����
|
||||
// �����������е��˱�����ʱ����
|
||||
OnAllEnemiesDefeated,
|
||||
// 新增:当单位恢复生命值时触发(治疗)
|
||||
// ����������λ�ָ�����ֵʱ���������ƣ�
|
||||
OnHPHealed,
|
||||
// 新增:当单位失去生命值时触发(受伤/被伤害)
|
||||
// ����������λʧȥ����ֵʱ����������/���˺���
|
||||
OnHPLost,
|
||||
// 新增:当单位获得法力时触发
|
||||
// ����������λ��÷���ʱ����
|
||||
OnManaGained,
|
||||
// 新增:当单位失去法力时触发
|
||||
// ����������λʧȥ����ʱ����
|
||||
OnManaLost,
|
||||
// 新增:当生命值高于某阈值时触发(按百分比,0-1)
|
||||
// ������������ֵ����ij��ֵʱ���������ٷֱȣ�0-1��
|
||||
OnHPAbovePercent,
|
||||
// 新增:当生命值低于某阈值时触发(按百分比,0-1)
|
||||
// ������������ֵ����ij��ֵʱ���������ٷֱȣ�0-1��
|
||||
OnHPBelowPercent
|
||||
}
|
||||
|
||||
[Header("标识 (Identity)")]
|
||||
[Tooltip("技能的内部唯一 ID,用于代码中识别此技能")]
|
||||
[Header("��ʶ (Identity)")]
|
||||
[Tooltip("���ܵ��ڲ�Ψһ ID�����ڴ�����ʶ��˼���")]
|
||||
public string skillId;
|
||||
|
||||
[Tooltip("技能在 UI 中显示的名称")]
|
||||
[Tooltip("������ UI ����ʾ������")]
|
||||
public string displayName;
|
||||
|
||||
[Tooltip("简短注释/摘要,仅供设计时参考,不影响运行")]
|
||||
[Tooltip("���ע��/ժҪ���������ʱ�ο�����Ӱ������")]
|
||||
[TextArea(1,2)]
|
||||
public string summaryInfo;
|
||||
|
||||
[Header("效果 (Effect)")]
|
||||
[Tooltip("技能的效果类型,例如即时伤害、持续伤害、治疗、增益/减益等")]
|
||||
[Header("�� (Effect)")]
|
||||
[Tooltip("���ܵ�Ч�����ͣ����缴ʱ�˺��������˺������ơ�����/�����")]
|
||||
public EffectType effectType = EffectType.DamageSingleEnemy;
|
||||
|
||||
[Tooltip("默认目标选择器:决定技能默认作用的目标范围(自身/所有友军/敌方等)")]
|
||||
[Tooltip("Ĭ��Ŀ��ѡ��������������Ĭ�����õ�Ŀ�귶Χ������/�����Ѿ�/�з��ȣ�")]
|
||||
public Selector defaultSelector = Selector.CurrentEnemies;
|
||||
|
||||
[Tooltip("当选择 FromFormula 时使用的表达式字符串。支持使用 AllyHero_SO 中同名数值字段作为变量,例如: attack, maxHP, maxMana, damageResistance, scoreEfficiency, ally_currentEXP, slot。示例: 'attack * 1.5 + maxHP * 0.1 + 20'。如果只填写一个数字(例如 '120'),则等同于指定固定数值。")]
|
||||
[Tooltip("��ѡ�� FromFormula ʱʹ�õı���ʽ�ַ�����֧��ʹ�ñ�����attack, maxHP, currentHP, maxMana, currentMana, damageResistance, scoreEfficiency, ally_currentEXP, currentScore(�� idolScore), slot, level/currentLevel/levelID��ʾ��: 'attack * 1.5 + maxHP * 0.1 + 20'�����ֻ��дһ�����֣����� '120'�������ͬ��ָ���̶���ֵ��")]
|
||||
[TextArea(2,4)]
|
||||
public string formula = "";
|
||||
|
||||
[Tooltip("默认持续时间(秒),用于持续效果或 Buff 的持续时长。若为 0 则视为一次性/即时技能(不会按 tick 分发)")]
|
||||
[Tooltip("Ĭ�ϳ���ʱ�䣨�룩�����ڳ���Ч���� Buff �ij���ʱ������Ϊ 0 ����Ϊһ����/��ʱ���ܣ����ᰴ tick �ַ���")]
|
||||
public float defaultDuration = 0f;
|
||||
|
||||
[Tooltip("默认打点间隔(秒),用于持续效果的每次触发间隔(例如每秒扣血)。若为 0 则视为一次性/即时技能")]
|
||||
[Tooltip("Ĭ�ϴ�������룩�����ڳ���Ч����ÿ�δ������������ÿ���Ѫ������Ϊ 0 ����Ϊһ����/��ʱ����")]
|
||||
public float defaultTickInterval = 1f;
|
||||
|
||||
[Header("标志 (Flags)")]
|
||||
[Tooltip("是否必须指定单体目标(true 表示此技能通常需要传入 specificTarget)")]
|
||||
[Header("��־ (Flags)")]
|
||||
[Tooltip("�Ƿ����ָ������Ŀ�꣨true ��ʾ�˼���ͨ����Ҫ���� specificTarget��")]
|
||||
public bool requiresSpecificTarget = false; // if true, skill usually targets a specific GameObject
|
||||
|
||||
[Tooltip("是否直接作用于目标的数据(true 表示 SkillBuilder 会直接调用 ICombatant/AllyCombatant,而非通过 EffectSystem 分发)")]
|
||||
[Tooltip("�Ƿ�ֱ��������Ŀ������ݣ�true ��ʾ SkillBuilder ��ֱ�ӵ��� ICombatant/AllyCombatant������ͨ�� EffectSystem �ַ���")]
|
||||
public bool operateDirectly = false; // if true, SkillBuilder will operate on ICombatant/AllyCombatant fields directly and update UI, instead of calling EffectSystem
|
||||
|
||||
[Tooltip("技能的描述或备注,便于设计时填写说明")]
|
||||
[Tooltip("���ܵ�������ע���������ʱ��д˵��")]
|
||||
[TextArea(8,4)]
|
||||
public string description;
|
||||
|
||||
[Header("触发条件 (Trigger)")]
|
||||
[Tooltip("技能触发条件:进入游戏时、击打音符时、法力值满时等")]
|
||||
[Header("�������� (Trigger)")]
|
||||
[Tooltip("���ܴ���������������Ϸʱ����������ʱ������ֵ��ʱ��")]
|
||||
public SkillTrigger triggerCondition = SkillTrigger.None;
|
||||
|
||||
[Tooltip("仅当 triggerCondition == OnNoteHit 时生效:达到该判定或更高时触发(可选 Miss/Good/Great/Perfect)。特别说明:选择 Miss 表示“仅在判定为 Miss 时触发”,其它判定(Good/Great/Perfect)不会触发。)")]
|
||||
[Tooltip("���� triggerCondition == OnNoteHit ʱ��Ч���ﵽ���ж������ʱ��������ѡ Miss/Good/Great/Perfect�����ر�˵����ѡ�� Miss ��ʾ�������ж�Ϊ Miss ʱ�������������ж���Good/Great/Perfect�����ᴥ������")]
|
||||
public NoteTriggerThreshold onNoteHitMinThreshold = NoteTriggerThreshold.Good;
|
||||
|
||||
[Tooltip("仅当 triggerCondition == OnNoteHit 时生效:指定此技能由 Tap(点击)/Hold(长按尾段)或 Either(任意)触发。若选择 Hold,则只有长音符尾段会触发;若选择 Tap,则仅短按触发;Either 则两者均可(但 Hold 仍可被显式禁用)。")]
|
||||
[Tooltip("���� triggerCondition == OnNoteHit ʱ��Ч��ָ���˼����� Tap�������/Hold������β�Σ��� Either�����⣩��������ѡ�� Hold����ֻ�г�����β�λᴥ������ѡ�� Tap������̰�������Either �����߾��ɣ��� Hold �Կɱ���ʽ���ã���")]
|
||||
public NoteTypeTrigger noteTriggerType = NoteTypeTrigger.Either;
|
||||
|
||||
[Tooltip("仅当 triggerCondition == OnNoteHit 时生效:触发后的冷却时间(秒),在冷却时间内再次满足判定不会触发技能")]
|
||||
[Tooltip("���� triggerCondition == OnNoteHit ʱ��Ч�����������ȴʱ�䣨�룩������ȴʱ�����ٴ������ж����ᴥ������")]
|
||||
public float onNoteHitCooldown = 0f;
|
||||
|
||||
[Tooltip("仅当 triggerCondition 为 HP 百分比相关时生效:生命值阈值(0-1),例如 0.3 表示 30%")]
|
||||
[Tooltip("���� triggerCondition Ϊ HP �ٷֱ����ʱ��Ч������ֵ��ֵ��0-1�������� 0.3 ��ʾ 30%")]
|
||||
public float hpTriggerPercent = 0.5f;
|
||||
|
||||
// Minimum judge quality required to trigger on note hit
|
||||
public enum NoteTriggerThreshold
|
||||
{
|
||||
// 当设置为 Miss 时,技能仅在判定为 Miss(quality == 0)时触发
|
||||
// ������Ϊ Miss ʱ�����ܽ����ж�Ϊ Miss��quality == 0��ʱ����
|
||||
Miss = 0,
|
||||
Good = 1,
|
||||
Great = 2,
|
||||
Perfect = 3
|
||||
}
|
||||
|
||||
// 新增:指定技能应由 Tap/ Hold/ Either 触发
|
||||
// ������ָ������Ӧ�� Tap/ Hold/ Either ����
|
||||
public enum NoteTypeTrigger
|
||||
{
|
||||
Tap,
|
||||
@@ -103,10 +107,10 @@ public class SkillDefinition : ScriptableObject
|
||||
Either
|
||||
}
|
||||
|
||||
// 运行时辅助属性:当 defaultDuration 或 defaultTickInterval 为 0 时,视作一次性即时技能
|
||||
// ����ʱ�������ԣ��� defaultDuration �� defaultTickInterval Ϊ 0 ʱ������һ���Լ�ʱ����
|
||||
public bool IsSingleInstance => defaultDuration <= 0f || defaultTickInterval <= 0f;
|
||||
|
||||
// 返回实际用于分发打点的间隔(若为一次性技能则返回 0)
|
||||
// ����ʵ�����ڷַ����ļ������Ϊһ���Լ����� 0��
|
||||
public float GetEffectiveTickInterval()
|
||||
{
|
||||
return IsSingleInstance ? 0f : Mathf.Max(0.0001f, defaultTickInterval);
|
||||
@@ -114,7 +118,7 @@ public class SkillDefinition : ScriptableObject
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
// 保证数值为非负
|
||||
// ��֤��ֵΪ�Ǹ�
|
||||
if (defaultDuration < 0f) defaultDuration = 0f;
|
||||
if (defaultTickInterval < 0f) defaultTickInterval = 0f;
|
||||
if (onNoteHitCooldown < 0f) onNoteHitCooldown = 0f;
|
||||
@@ -131,17 +135,30 @@ public class SkillDefinition : ScriptableObject
|
||||
if (string.IsNullOrWhiteSpace(expr)) return false;
|
||||
try
|
||||
{
|
||||
var tokens = Tokenize(expr);
|
||||
var rpn = ToRPN(tokens);
|
||||
if (s_badFormulaCache.Contains(expr))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s_rpnCache.TryGetValue(expr, out var rpn) || rpn == null)
|
||||
{
|
||||
var tokens = Tokenize(expr);
|
||||
rpn = ToRPN(tokens);
|
||||
s_rpnCache[expr] = rpn;
|
||||
}
|
||||
|
||||
result = EvalRPN(rpn, variables);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"SkillDefinition: failed to evaluate formula '{expr}': {e.Message}");
|
||||
if (!s_badFormulaCache.Contains(expr))
|
||||
{
|
||||
s_badFormulaCache.Add(expr);
|
||||
Debug.LogWarning($"SkillDefinition: failed to evaluate formula '{expr}': {e.Message}");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
// tokenization
|
||||
private enum TokType { Number, Ident, Op, LParen, RParen }
|
||||
@@ -283,3 +300,4 @@ public class SkillDefinition : ScriptableObject
|
||||
return st.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user