表达式引擎 (Expression Engine)
表达式引擎基于 exp4j 库,用来在运行时求值数学表达式。你可以在配置文件里写公式,引擎会帮你算出结果。除了基本的数学运算,它还支持变量替换和多种随机分布——做伤害公式、掉落数量计算之类的场景特别方便。
基本用法
传入一个数学表达式字符串,拿回 double 结果:
ExpressionService exprService = EmakiServiceRegistry.get(ExpressionService.class);
double result = exprService.evaluate("2 + 3 * 4"); // 14.0
double result2 = exprService.evaluate("ceil(3.2)"); // 4.0
double result3 = exprService.evaluate("max(10, 20)"); // 20.0内置函数
| 函数 | 说明 | 示例 | 结果 |
|---|---|---|---|
ceil(x) | 向上取整 | ceil(3.2) | 4.0 |
floor(x) | 向下取整 | floor(3.8) | 3.0 |
round(x) | 四舍五入 | round(3.5) | 4.0 |
log10(x) | 以 10 为底的对数 | log10(100) | 2.0 |
min(a, b) | 取最小值 | min(3, 7) | 3.0 |
max(a, b) | 取最大值 | max(3, 7) | 7.0 |
pow(a, b) | 幂运算 | pow(2, 10) | 1024.0 |
除了这些,exp4j 自带的标准数学函数也都能用:abs, sqrt, sin, cos, tan, asin, acos, atan, exp, log(自然对数)等。
变量语法
用 {varName} 格式在表达式里引用变量:
Map<String, Double> variables = Map.of(
"base_damage", 10.0,
"level", 5.0,
"multiplier", 1.5
);
double result = exprService.evaluate(
"{base_damage} + {level} * {multiplier}",
variables
);
// 结果: 10.0 + 5.0 * 1.5 = 17.5在 YAML 配置里写公式也是同样的语法:
damage_formula: "{base_damage} + {level} * 2 + pow({strength}, 0.5)"
heal_formula: "max({base_heal}, {level} * 0.8)"
crit_chance: "min(0.75, {base_crit} + {agility} * 0.01)"提示
变量名支持字母、数字和下划线,区分大小写。变量值必须是数值类型(double)。
随机分布配置
在配置文件里定义随机数值时,表达式引擎支持多种分布类型。不同的分布适合不同的场景——均匀分布适合"等概率随机",高斯分布适合"集中在某个值附近",三角分布适合"有一个最可能的值"。
constant — 常量
固定值,没有随机性。直接写数字就行:
# 直接写数值即为常量
drop_count: 5
# 或者显式声明
drop_count:
type: constant
value: 5range — 范围简写
用 min~max 格式快速定义一个均匀随机范围,这是最常用的写法:
# 5 到 15 之间的随机整数
drop_count: "5~15"
# 也支持小数
damage_bonus: "1.5~3.0"uniform — 均匀分布
和范围简写等价,只是用完整格式写出来:
drop_count:
type: uniform
min: 5
max: 15gaussian — 高斯(正态)分布
以 mean 为中心,stddev 为标准差。大部分结果会集中在均值附近,偶尔出现极端值。可以用 min / max 截断,防止出现不合理的数值。
damage:
type: gaussian
mean: 100
stddev: 15
min: 50 # 可选,最小截断值
max: 150 # 可选,最大截断值skew_normal — 偏态正态分布
在高斯分布的基础上加了偏斜参数 skew。正值右偏(高值更稀有),负值左偏(低值更稀有)。适合做稀有度相关的随机。
rare_drop_quality:
type: skew_normal
mean: 50
stddev: 20
skew: 3.0 # 正值:右偏,高品质更稀有
min: 0
max: 100triangle — 三角分布
由 min、max 和 mode(众数)定义。结果最可能落在 mode 附近,越远离 mode 概率越低。比高斯分布更直观,适合"大部分时候给个中等值,偶尔给高值或低值"的场景。
crafting_quality:
type: triangle
min: 0
max: 100
mode: 70 # 最可能出现的值expression — 表达式分布
用数学表达式自定义随机逻辑。表达式里可以用 {random} 变量,它是一个 0.0 ~ 1.0 的均匀随机数。
custom_damage:
type: expression
expression: "floor({base_damage} * (1 + {random} * 0.5))"
variables:
base_damage: 100字符串配置类型
除了数值分布,表达式引擎还支持对"字符串配置对象"进行求值。这在 Lore 渲染、技能参数等场景中使用,让你可以在配置里写出动态文本。
random_text — 随机文本
从候选行列表中随机抽取指定数量的文本行。适合做随机词条、随机描述等场景。
random_bonus_lines:
type: "random_text"
rolls: 2 # 抽取次数(支持数值配置,如 range/expression)
allow_duplicates: false # 是否允许重复抽取同一行
lines: # 候选行列表
- "物理攻击: +{物理攻击}"
- "物理防御: +{物理防御}"
variables: # 局部变量(支持数值配置类型)
物理攻击:
type: "range"
min: 2
max: 6
物理防御:
type: "uniform"
min: 1
max: 4| 字段 | 必填 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
type | 是 | string | — | 必须为 random_text(别名:random_text_lines、random_lines、random_line、text_lines) |
rolls | 否 | 数值配置 | 1 | 抽取次数,支持常量、range、expression 等数值配置类型 |
allow_duplicates | 否 | boolean | false | 是否允许同一行被重复抽取 |
lines | 是 | list | — | 候选文本行列表(别名:values、options、texts) |
variables | 否 | map | — | 局部变量定义,每个变量支持数值配置类型 |
separator | 否 | string | \n | 多行合并为单个字符串时的分隔符 |
提示
rolls 字段本身也支持数值配置类型。比如你可以写 rolls: "1~3" 来随机抽取 1 到 3 行,或者用 rolls: { type: expression, expression: "{level}" } 让抽取次数随等级变化。
注意
当 allow_duplicates: false 且 rolls 大于候选行数量时,最多只能抽取到全部候选行。
完整配置示例
把各种分布混在一起用的实际例子:
# 一个掉落物配置
drops:
diamond:
source: "vanilla-diamond"
count: "1~3" # 范围简写
gold:
source: "vanilla-gold_ingot"
count: # 高斯分布
type: gaussian
mean: 5
stddev: 2
min: 1
max: 10
rare_gem:
source: "ia-emaki:rare_gem"
count: # 三角分布
type: triangle
min: 0
max: 5
mode: 1
bonus_exp:
type: expression # 表达式
expression: "floor(100 * pow(1.1, {player_level}))"随机文本配置示例
技能参数中使用 random_text 生成随机词条:
skill_parameters:
random_bonus_lines:
type: "random_text"
rolls: "{bonus_rolls}"
allow_duplicates: false
variables:
bonus_rolls:
type: "constant"
value: 2
物理攻击:
type: "range"
min: 2
max: 6
物理防御:
type: "uniform"
min: 1
max: 4
lines:
- "物理攻击: +{物理攻击}"
- "物理防御: +{物理防御}"安全限制
为了防止恶意或写错的表达式拖慢服务器,引擎有以下限制:
| 限制项 | 值 | 说明 |
|---|---|---|
| 最大表达式长度 | 256 字符 | 超出直接拒绝求值 |
| 禁止字符 | ;, \, ", ' | 防止注入 |
| 最大嵌套深度 | 10 层 | 函数嵌套不能超过 10 层 |
| 表达式缓存 | L1: 256 条 / 30 分钟 TTL,L2: 全局 1024 条 | 双层缓存架构,L1 为线程本地,L2 为跨线程共享 |
注意
表达式求值在主线程执行。简单的表达式没什么问题,但如果在高频调用路径(比如每 tick 都算一次)中用了很复杂的表达式,可能会有性能影响。缓存能帮上忙,但首次编译还是有开销的。
// 超出限制时会抛出 ExpressionException
try {
double result = exprService.evaluate(veryLongExpression);
} catch (ExpressionException e) {
logger.warning("表达式求值失败: " + e.getMessage());
}提示
表达式缓存采用双层架构:L1 为线程本地缓存(LRU,256 条,30 分钟 TTL),L2 为全局共享缓存(ConcurrentHashMap,1024 条)。相同的表达式只编译一次,跨线程也能共享编译结果。对于反复求值的公式(比如伤害计算),缓存基本能消除编译开销。