304 lines
14 KiB
C#
304 lines
14 KiB
C#
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<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,
|
||
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<string, float> 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<Token> Tokenize(string s)
|
||
{
|
||
var outp = new List<Token>();
|
||
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<Token>();
|
||
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<Token> ToRPN(List<Token> tokens)
|
||
{
|
||
var output = new List<Token>();
|
||
var stack = new Stack<Token>();
|
||
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<Token> rpn, Dictionary<string, float> vars)
|
||
{
|
||
var st = new Stack<float>();
|
||
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();
|
||
}
|
||
}
|
||
|