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

266 lines
10 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
{
public enum SkillTrigger
{
None,
OnGameStart,
OnNoteHit,
OnManaFull,
OnEnemyRevive, // 新增:当敌人登场时触发
OnEnemyDead // 新增:当敌人死亡时触发
}
[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 时使用的表达式字符串。支持使用 AllyHero_SO 中同名数值字段作为变量,例如: attack, maxHP, maxMana, damageResistance, scoreEfficiency, ally_currentEXP, slot。示例: 'attack * 1.5 + maxHP * 0.1 + 20'。如果只填写一个数字(例如 '120'),则等同于指定固定数值。")]
[TextArea(2,4)]
public string formula = "";
[Tooltip("默认持续时间(秒),用于持续效果或 Buff 的持续时长。若为 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;
// 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;
}
// --- 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
{
var tokens = Tokenize(expr);
var rpn = ToRPN(tokens);
result = EvalRPN(rpn, variables);
return true;
}
catch (Exception e)
{
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();
}
}