技能主要更新,修复卡顿并加入动画,以及各种其他更新。

This commit is contained in:
FloatGaming
2026-02-07 21:05:17 +08:00
parent abeca51be5
commit 1ac3cd0104
1349 changed files with 1526749 additions and 24850 deletions
+335 -67
View File
@@ -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
}
}
}
+2 -1
View File
@@ -40,7 +40,8 @@ public enum EffectType
IncreaseDamageResistance,
DecreaseDamageResistance,
IncreaseAttack,
DecreaseAttack
DecreaseAttack,
RedirectNextDamageToSelf
}
public interface ICombatant
+76 -33
View File
@@ -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
}
+7 -7
View File
@@ -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();
+109 -40
View File
@@ -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.");
}
}
}
}
+146 -54
View File
@@ -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
}
}
}
+57 -39
View File
@@ -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 时,技能仅在判定为 Missquality == 0)时触发
// Ϊ Miss ʱܽжΪ Missquality == 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();
}
}