using UnityEngine; using System; using System.Collections.Generic; 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> s_rpnCache = new Dictionary>(); private static readonly HashSet s_badFormulaCache = new HashSet(); public enum SkillTrigger { None, OnGameStart, OnNoteHit, OnManaFull, OnAttackZero, OnEnemyRevive, // �����������˵dz�ʱ���� OnEnemyDead, // ����������������ʱ���� // �����������е��˱�����ʱ���� OnAllEnemiesDefeated, // ����������λ�ָ�����ֵʱ���������ƣ� OnHPHealed, // ����������λʧȥ����ֵʱ����������/���˺��� OnHPLost, // ����������λ��÷���ʱ���� OnManaGained, // ����������λʧȥ����ʱ���� OnManaLost, // ������������ֵ����ij��ֵʱ���������ٷֱȣ�0-1�� OnHPAbovePercent, // ������������ֵ����ij��ֵʱ���������ٷֱȣ�0-1�� OnHPBelowPercent } [Header("��ʶ (Identity)")] [Tooltip("���ܵ��ڲ�Ψһ ID�����ڴ�����ʶ��˼���")] public string skillId; [Tooltip("������ UI ����ʾ������")] public string displayName; [Tooltip("���ע��/ժҪ���������ʱ�ο�����Ӱ������")] [TextArea(1,2)] public string summaryInfo; [Header("Ч�� (Effect)")] [Tooltip("���ܵ�Ч�����ͣ����缴ʱ�˺��������˺������ơ�����/�����")] public EffectType effectType = EffectType.DamageSingleEnemy; [Tooltip("Ĭ��Ŀ��ѡ��������������Ĭ�����õ�Ŀ�귶Χ������/�����Ѿ�/�з��ȣ�")] public Selector defaultSelector = Selector.CurrentEnemies; [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 �ij���ʱ������Ϊ 0 ����Ϊһ����/��ʱ���ܣ����ᰴ tick �ַ���")] public float defaultDuration = 0f; [Tooltip("Ĭ�ϴ�������룩�����ڳ���Ч����ÿ�δ������������ÿ���Ѫ������Ϊ 0 ����Ϊһ����/��ʱ����")] public float defaultTickInterval = 1f; [Header("��־ (Flags)")] [Tooltip("�Ƿ����ָ������Ŀ�꣨true ��ʾ�˼���ͨ����Ҫ���� specificTarget��")] public bool requiresSpecificTarget = false; // if true, skill usually targets a specific GameObject [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("���ܵ�������ע���������ʱ��д˵��")] [TextArea(8,4)] public string description; [Header("�������� (Trigger)")] [Tooltip("���ܴ���������������Ϸʱ����������ʱ������ֵ��ʱ��")] public SkillTrigger triggerCondition = SkillTrigger.None; [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 �Կɱ���ʽ���ã���")] public NoteTypeTrigger noteTriggerType = NoteTypeTrigger.Either; [Tooltip("���� triggerCondition == OnNoteHit ʱ��Ч�����������ȴʱ�䣨�룩������ȴʱ�����ٴ������ж����ᴥ������")] public float onNoteHitCooldown = 0f; [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 = 0, Good = 1, Great = 2, Perfect = 3 } // ������ָ������Ӧ�� Tap/ Hold/ Either ���� public enum NoteTypeTrigger { Tap, Hold, Either } // ����ʱ�������ԣ��� defaultDuration �� defaultTickInterval Ϊ 0 ʱ������һ���Լ�ʱ���� public bool IsSingleInstance => defaultDuration <= 0f || defaultTickInterval <= 0f; // ����ʵ�����ڷַ����ļ������Ϊһ���Լ����򷵻� 0�� public float GetEffectiveTickInterval() { return IsSingleInstance ? 0f : Mathf.Max(0.0001f, defaultTickInterval); } private void OnValidate() { // ��֤��ֵΪ�Ǹ� if (defaultDuration < 0f) defaultDuration = 0f; if (defaultTickInterval < 0f) defaultTickInterval = 0f; if (onNoteHitCooldown < 0f) onNoteHitCooldown = 0f; // clamp hpTriggerPercent if (hpTriggerPercent < 0f) hpTriggerPercent = 0f; if (hpTriggerPercent > 1f) hpTriggerPercent = 1f; } // --- Simple expression evaluator used when valueMode == FromFormula --- // Supports variables (alphanumeric), floats, operators + - * / and parentheses. public static bool TryEvaluateFormula(string expr, Dictionary variables, out float result) { result = 0f; if (string.IsNullOrWhiteSpace(expr)) return false; try { 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) { 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 } private struct Token { public TokType type; public string text; } private static List Tokenize(string s) { var outp = new List(); int i = 0; while (i < s.Length) { char c = s[i]; if (char.IsWhiteSpace(c)) { i++; continue; } if (c == '(') { outp.Add(new Token { type = TokType.LParen, text = "(" }); i++; continue; } if (c == ')') { outp.Add(new Token { type = TokType.RParen, text = ")" }); i++; continue; } if ("+-*/".IndexOf(c) >= 0) { outp.Add(new Token { type = TokType.Op, text = c.ToString() }); i++; continue; } if (char.IsDigit(c) || c == '.') { int start = i; while (i < s.Length && (char.IsDigit(s[i]) || s[i] == '.')) i++; outp.Add(new Token { type = TokType.Number, text = s.Substring(start, i - start) }); continue; } if (char.IsLetter(c) || c == '_') { int start = i; while (i < s.Length && (char.IsLetterOrDigit(s[i]) || s[i] == '_')) i++; outp.Add(new Token { type = TokType.Ident, text = s.Substring(start, i - start) }); continue; } throw new Exception($"Invalid char in formula: '{c}'"); } // Handle unary plus/minus: when + or - appears at start or after operator or left paren var fixedTokens = new List(); for (int idx = 0; idx < outp.Count; idx++) { var t = outp[idx]; if (t.type == TokType.Op && (t.text == "-" || t.text == "+")) { bool unary = (idx == 0) || (outp[idx - 1].type == TokType.Op) || (outp[idx - 1].type == TokType.LParen); if (unary) { // insert 0 before unary +/- to convert unary into binary (0 +/- x) fixedTokens.Add(new Token { type = TokType.Number, text = "0" }); } } fixedTokens.Add(t); } return fixedTokens; } private static int Precedence(string op) { if (op == "+" || op == "-") return 1; if (op == "*" || op == "/") return 2; return 0; } private static List ToRPN(List tokens) { var output = new List(); var stack = new Stack(); foreach (var t in tokens) { if (t.type == TokType.Number || t.type == TokType.Ident) { output.Add(t); } else if (t.type == TokType.Op) { while (stack.Count > 0 && stack.Peek().type == TokType.Op && Precedence(stack.Peek().text) >= Precedence(t.text)) { output.Add(stack.Pop()); } stack.Push(t); } else if (t.type == TokType.LParen) { stack.Push(t); } else if (t.type == TokType.RParen) { while (stack.Count > 0 && stack.Peek().type != TokType.LParen) { output.Add(stack.Pop()); } if (stack.Count == 0) throw new Exception("Mismatched parentheses"); stack.Pop(); // pop LParen } } while (stack.Count > 0) { var tk = stack.Pop(); if (tk.type == TokType.LParen || tk.type == TokType.RParen) throw new Exception("Mismatched parentheses"); output.Add(tk); } return output; } private static float EvalRPN(List rpn, Dictionary vars) { var st = new Stack(); foreach (var t in rpn) { if (t.type == TokType.Number) { if (!float.TryParse(t.text, NumberStyles.Float, CultureInfo.InvariantCulture, out float v)) throw new Exception($"Invalid number '{t.text}'"); st.Push(v); } else if (t.type == TokType.Ident) { if (vars != null && vars.TryGetValue(t.text, out float v)) st.Push(v); else { // unknown identifiers treat as 0 but warn Debug.LogWarning($"SkillDefinition: unknown variable '{t.text}' in formula, treated as 0"); st.Push(0f); } } else if (t.type == TokType.Op) { if (st.Count < 2) throw new Exception("Insufficient values for operator"); float b = st.Pop(); float a = st.Pop(); switch (t.text) { case "+": st.Push(a + b); break; case "-": st.Push(a - b); break; case "*": st.Push(a * b); break; case "/": st.Push(a / b); break; default: throw new Exception($"Unsupported operator {t.text}"); } } else throw new Exception("Invalid token in RPN"); } if (st.Count != 1) throw new Exception("Invalid expression evaluation"); return st.Pop(); } }