script-js / AviatorScript
这一页先讲一个最重要的事实:
AchieveMaster 里的
script-js,名字虽然叫js,但实际执行引擎是 AviatorScript 5.4.3,不是浏览器 JavaScript,也不是 Node.js。
所以你在这里写的是 Aviator 表达式 / 脚本语法,不是 AST/TQS 中文语法。
当前有哪些入口
ConfigManager 当前支持这些配置入口:
| 位置 | 支持键名 | 说明 |
|---|---|---|
trigger | script-js / 脚本js / js | 触发阶段 AviatorScript |
rewards[] | script-js / 脚本js / js | 奖励阶段 AviatorScript |
scripts/functions/js/*.av | 文件 | Aviator 自定义函数 |
scripts/functions/js/*.js | 文件 | 仍然走 Aviator,自定义函数文件后缀兼容为 .js |
脚本内容本身支持三种 YAML 写法:
- 单行字符串
|多行字符串- 字符串列表,加载时会用换行拼接
它和 AST/TQS 的关系
当前正式关系应该这样理解:
- AST/TQS 是主执行路径
script-js是并存的 Aviator 执行入口- 两边共享同一套
FunctionRegistry - 两边上下文来源相同,但暴露出来的对象形态不一样
所以不要把 AST 文档里的这些关键字直接原样复制进 script-js:
判断循环遍历延迟执行导入类
script-js 里要按 Aviator 语法写。
执行时会注入什么
JsScriptEngine.execute(...) 运行时会向 Aviator 环境注入这些对象:
| 名称 | 何时注入 | 含义 |
|---|---|---|
ctx / context | 总是 | ScriptContext |
player / 玩家 | 有玩家时 | 当前玩家对象 |
achievement / 成就 | 有成就时 | 当前成就对象 |
progress / 进度 | 有进度时 | AchievementProgress 对象 |
event / 事件 | 有事件时 | 当前 Bukkit 事件对象 |
entity / 实体 | 有实体时 | 当前实体对象 |
item / 物品 | 有物品时 | 当前物品对象 |
plugin | 总是 | 插件主类实例 |
Bukkit | 总是 | org.bukkit.Bukkit.class |
fn / $fn | 总是 | 函数桥接对象 |
这里最容易和 AST 混淆的一点是:
- AST 里
progress默认是数字 script-js里progress是AchievementProgress对象
也就是说,在 script-js 里你可以直接写:
javascript
progress.getProgress()
progress.getPercentage(achievement.getCompletionTarget())而不是把 progress 当 AST 数字变量那样理解。
script-js 返回值到底有什么用
这是当前最容易被误解的边界。
ScriptService.executeJavaScript(...) 执行 script-js 后,会把 Aviator 的执行结果放进 ScriptResult.returnValue。 但是当前成就触发与奖励执行路径:
- 不会把
return true/false当成 AST 那种“继续 / 停止”控制信号 - 也不会自动把返回数字当作进度变化
当前实际结论是:
script-js的返回值会被保留- 但触发器 / 奖励主流程本身目前不消费这个返回值做判定
所以如果你想让 script-js 真正产生效果,应该直接调用内置函数或操作注入对象,例如:
增加进度(...)设置进度(...)增加目标进度(...)发送消息(...)执行指令(...)
而不是只写一个 return true 期待它自动推动成就流程。
内置函数在 script-js 里怎么用
JsScriptEngine 初始化时,会把当前 FunctionRegistry 里的函数名和别名都注册进 Aviator 全局环境。
所以这些调用是成立的:
javascript
获取金币()
世界是否白天()
取玩家变量("签到次数")
设置进度(10)
发送消息("&a脚本触发成功")这和 AST 共享的是同一套函数注册表,不是另一份独立 API。
需要注意的是,Aviator 注册阶段如果发现同名函数已存在,会跳过重复注册。
fn / $fn 桥接对象
除了“直接把函数名当全局函数调用”,运行时还额外注入了 fn / $fn。
它们是 JsFunctionBridge,支持这些方法:
| 写法 | 说明 |
|---|---|
fn.invoke("函数名", arg1, arg2) | 变参调用 |
fn.call("函数名", seq.list(arg1, arg2)) | 用数组 / 列表调用 |
fn.has("函数名") | 检查函数是否存在 |
fn.list() | 返回已注册函数名数组 |
示例:
javascript
if fn.has("获取金币") {
let money = fn.invoke("获取金币");
if money >= 1000 {
fn.invoke("发送消息", "&6你已经满足金币条件");
}
}fn.invoke(...) 最适合下面两种场景:
- 函数名是动态拼出来的
- 你不想让脚本在编写时直接依赖某个全局函数名
触发脚本写法建议
有副作用的正式写法
当前更推荐这样写:
yaml
trigger:
type: PLAYER_JOIN
script-js: |
if player == nil {
return nil;
}
if 获取金币() >= 1000 {
增加进度(1);
发送消息("&a欢迎回来,已为你推进成就进度");
}这段脚本真正推动进度的,是 增加进度(1),不是 return。
直接访问注入对象
yaml
trigger:
type: PLAYER_CHAT
script-js: |
if event == nil {
return nil;
}
let msg = event.getMessage();
if string.contains(msg, "签到") {
增加进度(1);
}奖励脚本写法建议
奖励阶段也是同样逻辑。 当前奖励 script-js 的返回值不会被奖励系统额外消费,所以重点还是直接执行动作。
yaml
rewards:
- type: SCRIPT
script-js: |
发送消息("&6你完成了成就奖励脚本");
执行指令("say " + player.getName() + " 完成了奖励脚本");Aviator 自定义函数文件
CustomFunctionLoader 会从 scripts/functions/js/ 读取 .av 和 .js 文件,然后交给 JsFunctionLoader。
文件本质
这里的 .js 只是兼容后缀,不代表它是浏览器 JavaScript。 .av 和 .js 最终都走 Aviator。
加载时会做什么
AviatorUtil.loadCustomFunctions(script) 会:
- 注册
registerFunction - 注册
registerFunctionEx - 用一个空环境执行整份脚本
- 收集你在文件里声明的函数定义
这意味着一个重要边界:
- 自定义函数文件的顶层代码会在“加载阶段”执行
- 此时没有真实的
player/event/ctx运行时上下文
所以函数文件顶层应该只做“注册函数”,不要写依赖实时上下文的副作用逻辑。
registerFunction
当前源码支持三种主要形态:
两参数
name + lambda
javascript
registerFunction("示例JS_是否白天", lambda(ctx) ->
ctx.player != nil && ctx.player.getWorld().getTime() < 13000
end);三参数
name + category + lambda
javascript
registerFunction("示例JS_玩家名", "玩家", lambda(ctx) ->
ctx.player == nil ? "" : ctx.player.getName()
end);四参数
name + category + description + lambda
javascript
registerFunction("示例JS_获取玩家血量百分比", "玩家", "返回玩家当前血量百分比",
lambda(ctx) ->
if ctx.player == nil {
return 0;
}
let hp = ctx.player.getHealth();
let maxHp = ctx.player.getMaxHealth();
return math.round(hp / maxHp * 100);
end
);这里的第一个参数 ctx 很重要。
JsFunctionLoader 在真正调用自定义 Aviator 函数时,会把 ScriptContext 作为第一个参数手动塞进去。 所以自定义函数里请把第一个参数当作上下文对象来用。
registerFunctionEx
复杂写法可以用一个 map:
javascript
registerFunctionEx({
"name": "示例JS_计算伤害加成",
"category": "战斗",
"description": "根据玩家等级计算加成",
"aliases": seq.list("calcDamageBonus", "伤害加成"),
"execute": lambda(ctx, baseDamage, levelCoef) ->
if ctx.player == nil {
return baseDamage;
}
let coef = levelCoef == nil ? 1.0 : levelCoef;
let bonus = 1 + (ctx.player.getLevel() * coef / 100);
return math.round(baseDamage * bonus * 100) / 100;
end
});当前加载器真正会读取的键只有:
| 键 | 说明 |
|---|---|
name | 函数名 |
category | 分类 |
description | 描述 |
aliases | 别名列表 |
execute | lambda 主体 |
自定义函数的运行语义
自定义 Aviator 函数注册到 FunctionRegistry 之后,AST 和 script-js 都可以调用它们。
调用时实际参数顺序是:
ctx- 你在脚本里传入的其余参数
也就是说,如果你定义:
javascript
registerFunction("示例JS_相加", lambda(ctx, a, b) -> a + b end);那在 AST 或 Aviator 里调用 示例JS_相加(1, 2) 时,函数体里拿到的是:
ctx= 当前ScriptContexta=1b=2
访问 Java / Bukkit 类
因为环境里注入了 Bukkit 类对象,同时 Aviator 也能直接访问 Java 对象方法,所以这类写法是成立的:
javascript
let online = Bukkit.getOnlinePlayers().size();
let world = player.getWorld();
let name = world.getName();如果你要用其他 Java 类,通常直接写全限定类名更稳:
javascript
let Particle = org.bukkit.Particle;
let particle = Particle.valueOf(string.toUpperCase("flame"));
player.getWorld().spawnParticle(particle, player.getLocation(), 10);和 AST 最容易混淆的差异
| 主题 | AST/TQS | script-js |
|---|---|---|
| 执行引擎 | 自研 AST | AviatorScript 5.4.3 |
progress | 数字 | AchievementProgress 对象 |
| 流程语法 | 判断 / 循环 / 遍历 | if / let / lambda 等 Aviator 语法 |
| 反射入口 | 导入类 + new + 方法链 | 直接操作 Java 对象 / 类对象 |
| 返回值 | AST 控制流和 ScriptResult 深度整合 | 当前触发 / 奖励主流程基本不消费返回值 |
什么时候更适合用 script-js
更适合:
- 你已经熟悉 Aviator 语法
- 需要 lambda 风格自定义函数
- 想直接操作注入对象和 Java API
- 只是做一小段表达式型逻辑或桥接逻辑
不太适合:
- 团队主维护语言是 AST/TQS
- 你希望所有脚本都统一中文关键字风格
- 你期待它和 AST 一样有完整的块控制流语义
使用建议
- 把它当 AviatorScript,不要当浏览器 JS。
- 触发 / 奖励脚本如果要产生效果,优先直接调用内置函数,不要只写
return true/false。 - 共享逻辑优先抽到
scripts/functions/js/自定义函数文件。 - 自定义函数文件顶层只做注册,不要写依赖实时上下文的执行逻辑。
- 如果只是普通成就主逻辑,优先继续用 AST/TQS;
script-js更适合作为补充入口。