Files
bansonic_beta_main/Assets/scripts/Combat/SkillDefinition.cs
T

304 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ʱܽжΪ Missquality == 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();
}
}