编队系统大更新 基本快搞好了 准备做敌人和分数

This commit is contained in:
FloatGaming
2025-11-10 22:41:13 +08:00
parent 7837c6adf0
commit 941b294fce
98 changed files with 8933 additions and 98 deletions
+263
View File
@@ -0,0 +1,263 @@
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
}
[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();
}
}