Skip to content

表达式引擎 (Expression Engine)

表达式引擎基于 exp4j 库,用来在运行时求值数学表达式。你可以在配置文件里写公式,引擎会帮你算出结果。除了基本的数学运算,它还支持变量替换和多种随机分布——做伤害公式、掉落数量计算之类的场景特别方便。

基本用法

传入一个数学表达式字符串,拿回 double 结果:

java
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} 格式在表达式里引用变量:

java
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 配置里写公式也是同样的语法:

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 — 常量

固定值,没有随机性。直接写数字就行:

yaml
# 直接写数值即为常量
drop_count: 5

# 或者显式声明
drop_count:
  type: constant
  value: 5

range — 范围简写

min~max 格式快速定义一个均匀随机范围,这是最常用的写法:

yaml
# 5 到 15 之间的随机整数
drop_count: "5~15"

# 也支持小数
damage_bonus: "1.5~3.0"

uniform — 均匀分布

和范围简写等价,只是用完整格式写出来:

yaml
drop_count:
  type: uniform
  min: 5
  max: 15

gaussian — 高斯(正态)分布

mean 为中心,stddev 为标准差。大部分结果会集中在均值附近,偶尔出现极端值。可以用 min / max 截断,防止出现不合理的数值。

yaml
damage:
  type: gaussian
  mean: 100
  stddev: 15
  min: 50       # 可选,最小截断值
  max: 150      # 可选,最大截断值

skew_normal — 偏态正态分布

在高斯分布的基础上加了偏斜参数 skew。正值右偏(高值更稀有),负值左偏(低值更稀有)。适合做稀有度相关的随机。

yaml
rare_drop_quality:
  type: skew_normal
  mean: 50
  stddev: 20
  skew: 3.0     # 正值:右偏,高品质更稀有
  min: 0
  max: 100

triangle — 三角分布

minmaxmode(众数)定义。结果最可能落在 mode 附近,越远离 mode 概率越低。比高斯分布更直观,适合"大部分时候给个中等值,偶尔给高值或低值"的场景。

yaml
crafting_quality:
  type: triangle
  min: 0
  max: 100
  mode: 70      # 最可能出现的值

expression — 表达式分布

用数学表达式自定义随机逻辑。表达式里可以用 {random} 变量,它是一个 0.0 ~ 1.0 的均匀随机数。

yaml
custom_damage:
  type: expression
  expression: "floor({base_damage} * (1 + {random} * 0.5))"
  variables:
    base_damage: 100

字符串配置类型

除了数值分布,表达式引擎还支持对"字符串配置对象"进行求值。这在 Lore 渲染、技能参数等场景中使用,让你可以在配置里写出动态文本。

random_text — 随机文本

从候选行列表中随机抽取指定数量的文本行。适合做随机词条、随机描述等场景。

yaml
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
字段必填类型默认值说明
typestring必须为 random_text(别名:random_text_linesrandom_linesrandom_linetext_lines
rolls数值配置1抽取次数,支持常量、range、expression 等数值配置类型
allow_duplicatesbooleanfalse是否允许同一行被重复抽取
lineslist候选文本行列表(别名:valuesoptionstexts
variablesmap局部变量定义,每个变量支持数值配置类型
separatorstring\n多行合并为单个字符串时的分隔符

提示

rolls 字段本身也支持数值配置类型。比如你可以写 rolls: "1~3" 来随机抽取 1 到 3 行,或者用 rolls: { type: expression, expression: "{level}" } 让抽取次数随等级变化。

注意

allow_duplicates: falserolls 大于候选行数量时,最多只能抽取到全部候选行。

完整配置示例

把各种分布混在一起用的实际例子:

yaml
# 一个掉落物配置
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 生成随机词条:

yaml
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 都算一次)中用了很复杂的表达式,可能会有性能影响。缓存能帮上忙,但首次编译还是有开销的。

java
// 超出限制时会抛出 ExpressionException
try {
    double result = exprService.evaluate(veryLongExpression);
} catch (ExpressionException e) {
    logger.warning("表达式求值失败: " + e.getMessage());
}

提示

表达式缓存采用双层架构:L1 为线程本地缓存(LRU,256 条,30 分钟 TTL),L2 为全局共享缓存(ConcurrentHashMap,1024 条)。相同的表达式只编译一次,跨线程也能共享编译结果。对于反复求值的公式(比如伤害计算),缓存基本能消除编译开销。

Released under the GPL-3.0 License