KubeJS 可操作 UI(内置 Chest GUI:1.20.1 官方方案)¶
原文:原创教程(ぴよまる),API 依据 KubeJS 6.1(Minecraft 1.20.1)源码
dev.latvian.mods.kubejs.gui包
1.20.1 想要"能点能放"的可操作 UI,不用装任何第三方插件——KubeJS 6.1 官方内置了一套 GUI 系统:player.openChestGUI() 能打开一个自定义箱子界面(1~6 行格子),每个格子可以放物品、设按钮、绑点击回调;还有 player.openGUI() 和 player.openInventoryGUI() 做更自由的容器菜单。
✅ 版本说明:这是 KubeJS 6.1(Minecraft 1.20.1)内置功能,开箱即用,Forge/Fabric 均可。第三方 ScreenJS 只支持 1.19.2,1.20.1 请用本方案。
1. 最简单的用法:打开一个自定义箱子界面¶
// server_scripts/shop_gui.js
ItemEvents.rightClicked('kubejs:shop_wand', event => { // 右键"商店法杖"时打开 GUI
// openChestGUI(标题, 行数(1~6), 配置回调):打开一个自定义箱子界面
event.player.openChestGUI('我的商店', 3, gui => { // 3 行 = 27 个格子
// 在 (4, 1) 格子放一个"按钮":显示为钻石,点击后执行回调
gui.button(4, 1, 'minecraft:diamond', '买钻石', click => {
click.slot.gui.player.give('1x minecraft:diamond') // 给玩家 1 个钻石
click.slot.gui.player.tell('§a你买了一个钻石!') // 提示玩家
})
})
})
💡
button(x, y, 物品, 名称, 回调):x是列(0~8)、y是行(0~行数-1);格子里显示那个物品,鼠标移上去显示名称,左键点击触发回调。
2. 槽位操作(setItem / getItem / 点击绑定)¶
2.1 在指定格子里放物品¶
ItemEvents.rightClicked('kubejs:wand', event => { // 右键法杖打开界面
event.player.openChestGUI('展示柜', 1, gui => { // 1 行 = 9 格
// 用 getSlot(x, y) 拿到格子,setItem() 放物品进去
gui.getSlot(0, 0).setItem('minecraft:diamond') // 第 1 格放钻石
gui.getSlot(1, 0).setItem('2x minecraft:gold_ingot') // 第 2 格放 2 个金锭
gui.getSlot(2, 0).setItem('minecraft:stick') // 第 3 格放木棍
})
})
2.2 给格子绑定"点击行为"(三种点击分别处理)¶
ItemEvents.rightClicked('kubejs:wand', event => { // 右键法杖打开界面
event.player.openChestGUI('技能书', 1, gui => { // 1 行界面
let slot = gui.getSlot(0, 0) // 拿到第 1 个格子
slot.setItem('minecraft:book') // 格子里放一本书
slot.setLeftClicked(click => { // 左键点击
click.slot.gui.player.tell('你点了左键!')
})
slot.setRightClicked(click => { // 右键点击
click.slot.gui.player.tell('你点了右键!')
})
slot.setShiftLeftClicked(click => { // Shift+左键
click.slot.gui.player.tell('你 Shift 左键了!')
})
slot.setMiddleClicked(click => { // 中键点击
click.slot.gui.player.tell('你点了中键!')
})
})
})
可用的点击绑定方法:
| 方法 | 触发方式 |
|---|---|
setLeftClicked(cb) |
左键点击 |
setRightClicked(cb) |
右键点击 |
setMiddleClicked(cb) |
鼠标中键点击 |
setShiftLeftClicked(cb) |
Shift + 左键 |
setShiftRightClicked(cb) |
Shift + 右键 |
setDoubleClicked(cb) |
双击 |
setSwapped(cb) |
按交换键(默认 F)点击 |
setThrown(cb) |
丢弃键点击(默认 Q) |
3. 区域批量配置(slot 快速填充)¶
ItemEvents.rightClicked('kubejs:wand', event => { // 右键法杖打开界面
event.player.openChestGUI('宝石展架', 2, gui => { // 2 行界面
// 批量给 (0,0) 到 (8,1) 全部 18 个格子放物品
gui.slot(0, 0, 8, 1, slot => {
slot.setItem('minecraft:emerald') // 每个格子都放绿宝石
slot.setLeftClicked(click => { // 每个格子点击都给 1 个绿宝石
click.slot.gui.player.give('1x minecraft:emerald')
})
})
})
})
4. 全局回调(任何格子点击 / 点玩家背包 / 关闭界面)¶
ItemEvents.rightClicked('kubejs:wand', event => { // 右键法杖打开界面
event.player.openChestGUI('事件演示', 2, gui => { // 2 行界面
// 任意一个自定义格子被点击时触发(包括没绑回调的格子)
gui.anyClicked = click => {
console.log('点了格子 (' + click.slot.x + ',' + click.slot.y + ')')
}
// 玩家点自己背包里的物品时触发
gui.inventoryClicked = click => {
console.log('点了背包格子')
}
// 界面关闭时触发
gui.closed = () => {
console.log('界面已关闭')
}
})
})
⚠️
gui.anyClicked只在界面自己的格子上触发;玩家背包区域用gui.inventoryClicked。想阻止默认行为可以调click.setHandled()。
5. 完整案例:迷你商店(卖东西给玩家)¶
// server_scripts/mini_shop.js
// 效果:右键商店法杖 → 打开 2 行商店界面,点商品格子就花钱(经验)买东西
ItemEvents.rightClicked('kubejs:shop_wand', event => { // 监听"右键商店法杖"
let player = event.player // 拿到玩家
player.openChestGUI('迷你商店', 2, gui => { // 2 行 = 18 格
// 商品 1:钻石,价格 5 级经验
gui.button(0, 0, 'minecraft:diamond', '钻石 · 5 级经验', click => {
let p = click.slot.gui.player // 点击的玩家
if (p.experienceLevel >= 5) { // 经验够吗?
p.addExperienceLevels(-5) // 扣 5 级经验
p.give('1x minecraft:diamond') // 给钻石
p.tell('§a购买成功:钻石!')
} else {
p.tell('§c经验不足!需要 5 级。')
}
})
// 商品 2:金苹果,价格 10 级经验
gui.button(1, 0, 'minecraft:golden_apple', '金苹果 · 10 级经验', click => {
let p = click.slot.gui.player
if (p.experienceLevel >= 10) {
p.addExperienceLevels(-10)
p.give('1x minecraft:golden_apple')
p.tell('§a购买成功:金苹果!')
} else {
p.tell('§c经验不足!需要 10 级。')
}
})
// 关闭时提示
gui.closed = () => player.tell('§7商店已关闭,欢迎下次光临!')
})
})
6. 更自由的 openGUI / openInventoryGUI¶
如果箱子样式不够用,还有两个进阶入口(源码 kjs$openGUI / kjs$openInventoryGUI):
// openInventoryGUI:直接给一个物品容器(Inventory)当界面
ItemEvents.rightClicked('kubejs:bag', event => { // 右键"袋子"物品
let inv = event.player.persistentData.bag || { slots: [] } // 自己的存档当容器
event.player.openInventoryGUI(inv, '我的背包') // 打开容器界面
})
// openGUI:完全自定义尺寸的容器菜单(宽度/高度/标题)
ItemEvents.rightClicked('kubejs:custom', event => {
event.player.openGUI(gui => {
gui.title = '自定义界面' // 标题
gui.width = 176 // 界面宽度(像素)
gui.height = 166 // 界面高度(像素)
gui.inventory = /* 一个 Inventory 对象 */ // 可选:绑定物品容器
})
})
💡
openGUI的自由度最高,适合进阶玩家;日常做商店/菜单用openChestGUI就够了。
7. 翻页(分页):东西多了一页放不下怎么办¶
核心思路:openChestGUI 的 gui 对象只在打开时构建一次,不会自己刷新。所以翻页最省事的做法是——
页码存在变量里 → 点「下一页」时改页码 → 直接重新打开一次界面重画。
// server_scripts/paged_shop.js
const PAGE_SIZE = 45 // 每页 45 格(9 列 × 5 行),最后一行留给翻页按钮
// 商品列表(真实项目里一般从 JSON 读,见第 9 节)
const GOODS = [
{ icon: 'minecraft:diamond', name: '钻石', price: 100 },
{ icon: 'minecraft:gold_ingot', name: '金锭', price: 50 },
{ icon: 'minecraft:iron_ingot', name: '铁锭', price: 10 },
// …可以放几百条,翻页会自动处理
]
/** 打开第 page 页(page 从 0 开始) */
function openShopPage(player, page) {
const rows = 6 // 6 行 = 54 格
const maxPage = Math.max(1, Math.ceil(GOODS.length / PAGE_SIZE))
page = Math.max(0, Math.min(maxPage - 1, page)) // 夹住页码,防止越界
player.openChestGUI(`商品列表(第 ${page + 1}/${maxPage} 页)`, rows, gui => {
// ① 本页内容:从 page * PAGE_SIZE 开始,最多铺 PAGE_SIZE 个
const start = page * PAGE_SIZE
GOODS.slice(start, start + PAGE_SIZE).forEach((goods, i) => {
const x = i % 9 // 列:0~8
const y = Math.floor(i / 9) // 行:0~4
gui.button(x, y, goods.icon, `${goods.name} · ${goods.price} 币`, click => {
const p = click.slot.gui.player // 点击回调里拿玩家
if (p.persistentData.money === undefined) p.persistentData.money = 0
if (p.persistentData.money < goods.price) {
p.tell('§c余额不足!')
return
}
p.persistentData.money -= goods.price // 扣钱(真实项目建议存 JSON,见第 9 节)
p.give('1x ' + goods.icon)
p.tell(`§a购买成功:${goods.name}`)
})
})
// ② 翻页按钮:放在最下面一行(y = rows - 1)
const bottom = rows - 1
if (page > 0) {
gui.button(0, bottom, 'minecraft:arrow', '« 上一页', click => {
openShopPage(click.slot.gui.player, page - 1) // 重新打开上一页
})
}
if (page < maxPage - 1) {
gui.button(8, bottom, 'minecraft:arrow', '下一页 »', click => {
openShopPage(click.slot.gui.player, page + 1)
})
}
// ③ 页码指示(占位物品,点它没反应 → 传个空回调即可)
gui.button(4, bottom, 'minecraft:paper', `第 ${page + 1} / ${maxPage} 页`, () => {})
})
}
// 右键「商店法杖」打开第一页
ItemEvents.rightClicked('kubejs:shop_wand', event => {
openShopPage(event.player, 0)
})
💡 要点 - 翻页按钮一定放在每页都固定的位置(比如最后一行),这样翻页时按钮不会跟着内容乱跑。 - 页码用
Math.max/min夹住范围,避免玩家连点造成负数/越界。 - 想「翻页不掉焦点」:gui.closed里记住页码,下次打开直接回到那一页。 - 数据量大(几百条以上)时,用Array.prototype.slice切片就够,不用特意做懒加载。
8. 自动排版:一行代码把格子铺整齐¶
手写 x = i % 9, y = Math.floor(i / 9) 每次都写太烦,建议抽成一个通用排版函数:
自动计算列数、水平居中、首行留边距、并且支持「一页放不下就自动分页」。
// server_scripts/auto_layout.js
/**
* 自动把条目铺进箱子界面
* @param gui openChestGUI 给的回调参数
* @param items 条目数组:[{ icon:'物品id', name:'显示名', onClick(click){} }]
* @param opt 可选配置:
* rows 界面行数(默认 6)
* cols 每行几列(默认 9,箱子固定 9,一般不用改)
* page 当前页(从 0 开始,默认 0)
* perPage 每页最多几个(不填 = (rows-1)*cols,自动给底部留一行)
* center 是否水平居中(默认 true,条目不满一行时好看)
* topMargin 首行留几行空(默认 0)
* @returns { page, maxPage, total } 当前页/总页数/总条目
*/
function layoutChest(gui, items, opt = {}) {
const cols = opt.cols || 9
const rows = opt.rows || 6
const reserve = opt.reserve === undefined ? 1 : opt.reserve // 底部预留行数(给翻页按钮)
const perPage = opt.perPage || (rows - reserve) * cols
const maxPage = Math.max(1, Math.ceil(items.length / perPage))
const page = Math.max(0, Math.min(maxPage - 1, opt.page || 0))
const top = opt.topMargin || 0
const slice = items.slice(page * perPage, (page + 1) * perPage)
slice.forEach((it, i) => {
const row = Math.floor(i / cols) // 第几行(从 0 开始)
const inRow = i % cols // 行内第几个
const count = Math.min(cols, slice.length - row * cols) // 这一行实际几个(用于居中)
const offset = (opt.center === false || count >= cols) ? 0 : Math.floor((cols - count) / 2)
const x = inRow + offset
const y = row + top
if (it.onClick) {
gui.button(x, y, it.icon || 'minecraft:stone', it.name || '', click => it.onClick(click))
} else {
gui.getSlot(x, y).setItem(it.icon || 'minecraft:stone') // 纯展示,不绑点击
}
})
return { page, maxPage, total: items.length }
}
用法(配合翻页,10 行就够):
// server_scripts/chest_paged.js
const GOODS = [ /* … 一堆 {icon, name, price} … */ ]
function openGoods(player, page) {
const rows = 6
player.openChestGUI('商品', rows, gui => {
// 底部预留 1 行给翻页按钮 → 内容区 5 行
const info = layoutChest(gui, GOODS.map(g => ({
icon: g.icon,
name: `${g.name} · ${g.price} 币`,
onClick: click => buy(click.slot.gui.player, g) // hmm: 这里直接用 g 就行
})), { rows: rows, page: page })
// 翻页按钮(和排版函数配套:底部那一行)
const bottom = rows - 1
if (info.page > 0) gui.button(0, bottom, 'minecraft:arrow', '« 上一页',
click => openGoods(click.slot.gui.player, info.page - 1))
if (info.page < info.maxPage - 1) gui.button(8, bottom, 'minecraft:arrow', '下一页 »',
click => openGoods(click.slot.gui.player, info.page + 1))
gui.button(4, bottom, 'minecraft:paper', `第 ${info.page + 1} / ${info.maxPage} 页`, () => {})
})
}
💡 排版小技巧 - 居中:条目不满一行时算一下
(9 - 本行数量) / 2的偏移,看起来立刻整齐很多(center: false可关)。 - 留边距:topMargin空出第一行做标题/说明;reserve空出底部一行放翻页/关闭按钮。 - 一行代码切换布局:cols改成 7 就会变成「每行 7 个、右边留空」的样式(适合小图标)。 - 想加「分类标签页」:把items按分类过滤后再传给layoutChest,页签本身用最上面一行做按钮。
9. 用 JSON 存数据(KubeJS 6.1 的 JsonIO)¶
界面里的数据(价格表、玩家余额、排行榜、已解锁列表…)如果只放在脚本变量里,重启服务器就没了。
KubeJS 6.1 内置 JsonIO,读写 JSON 文件非常方便(不需要额外装任何东西)。
9.1 三个最常用的方法¶
| 方法 | 作用 | 返回 |
|---|---|---|
JsonIO.read(路径) |
读 JSON 文件 → 直接得到 JS 对象(Map/List/数字/字符串) | 对象 |
JsonIO.write(路径, 对象) |
把对象写成 JSON 文件(自动 pretty 缩进) | 无 |
JsonIO.of(值) |
把任何 JS 值转成 JSON 结构(写入前包一层更稳) | JsonElement |
JsonIO.toString(值) / toPrettyString(值) |
转成 JSON 字符串(打日志/调试用) | 字符串 |
JsonIO.parse(字符串) |
字符串 → JS 对象 | 对象 |
路径规则:相对「服务器运行目录」(就是
run/那一层)。惯例放在kubejs/data/下,例如kubejs/data/my_shop.json。
9.2 最小心智模型:读 → 改 → 写¶
// server_scripts/save_demo.js
const FILE = 'kubejs/data/my_shop.json' // 数据文件
// ① 读(文件可能不存在 → 用 try/catch 兜住,给一份默认值)
function loadShop() {
try {
const data = JsonIO.read(FILE) // 读到就是普通 JS 对象
if (data && data.items) return data // 有内容就用它
} catch (e) {
console.warn('[商店] 读取失败(首次运行正常):' + e)
}
return { version: 1, items: [], stats: { sold: 0 } } // 默认数据
}
// ② 改(就是普通对象操作,怎么改都行)
let shop = loadShop()
shop.items.push({ id: 'minecraft:diamond', name: '钻石', price: 100 })
shop.stats.sold++
// ③ 写(JsonIO.of 把 JS 对象转成 JSON 再落盘)
JsonIO.write(FILE, JsonIO.of(shop))
9.3 案例 A:商店价目表(改完立刻生效,不用重启)¶
// server_scripts/shop_catalog.js
const CATALOG = 'kubejs/data/shop_catalog.json'
function catalog() {
try { return JsonIO.read(CATALOG) } catch (e) { return null }
}
// 首次运行自动生成一份默认价目表
ServerEvents.loaded(event => {
if (!catalog()) {
JsonIO.write(CATALOG, JsonIO.of({
version: 1,
items: [
{ id: 'minecraft:diamond', name: '钻石', price: 100 },
{ id: 'minecraft:gold_ingot', name: '金锭', price: 50, limitPerDay: 16 },
{ id: 'minecraft:iron_ingot', name: '铁锭', price: 10 }
]
}))
console.info('[商店] 已生成默认价目表 ' + CATALOG)
}
})
// 打开界面时读一次(每次打开都是最新的,管理员改完存盘就生效)
ItemEvents.rightClicked('kubejs:shop_wand', event => {
const data = catalog() || { items: [] }
event.player.openChestGUI('商店', 3, gui => {
layoutChest(gui, data.items.map(g => ({
icon: g.id, name: `${g.name} · ${g.price} 币`,
onClick: click => buy(click.slot.gui.player, g)
})), { rows: 3, reserve: 0 })
})
})
9.4 案例 B:一人一档(按 UUID 分文件)+ 与 persistentData 的取舍¶
// server_scripts/player_json.js
// 每个玩家一个文件:kubejs/data/players/<uuid>.json
function playerFile(p) { return 'kubejs/data/players/' + p.uuid + '.json' }
function loadPlayer(p) {
try { return JsonIO.read(playerFile(p)) } catch (e) { /* 新玩家 */ }
return { money: 0, kills: 0, owned: [] }
}
function savePlayer(p, data) {
JsonIO.write(playerFile(p), JsonIO.of(data))
}
// 示例:击杀 +1 并存盘
EntityEvents.death('minecraft:player', event => {
const killer = event.source.actual // hmm: 击杀者
if (!killer || !killer.isPlayer || !killer.isPlayer()) return
const d = loadPlayer(killer)
d.kills = (d.kills || 0) + 1
savePlayer(killer, d)
})
什么时候用 JSON、什么时候用 persistentData?
JSON 文件(JsonIO) |
persistentData(NBT) |
|
|---|---|---|
| 存哪 | kubejs/data/xxx.json |
玩家/方块实体自带存档 |
| 好处 | 能用记事本直接改、能跨服共享、好备份、好发给别人 | 零成本、自动跟人走、不可能写丢 |
| 坏处 | 要自己管读写时机、文件多了要小心性能 | 只能存 NBT 类型、手改不方便 |
| 适合 | 价目表 / 活动配置 / 跨服数据 / 想手改的东西 | 玩家余额、计分、临时状态 |
💡 常见组合:玩家余额放
persistentData(自动跟人走),价目表/活动配置放 JSON(方便手改)。
9.5 案例 C:排行榜(JSON 累加 + 界面翻页展示)¶
// server_scripts/leaderboard.js
const LB = 'kubejs/data/leaderboard.json'
function loadLb() {
try { return JsonIO.read(LB) } catch (e) { return { top: {} } }
}
// 击杀记录
PlayerEvents.deathx(event => {}) // 占位,见下方真实写法
// 真正的写法:实体死亡事件
EntityEvents.death(event => {
const killer = event.source.player // 击杀者(可能是 null)
if (!killer) return
const lb = loadLb()
const key = killer.username
lb.top[key] = (lb.top[key] || 0) + 1 // 累加
JsonIO.write(LB, JsonIO.of(lb))
})
// 用界面看排行(按击杀数排序 + 自动排版 + 翻页)
ItemEvents.rightClicked('kubejs:lb_wand', event => {
const list = Object.entries(loadLb().top || {})
.map(([name, kills]) => ({ name, kills }))
.sort((a, b) => b.kills - a.kills)
const page = 0
event.player.openChestGUI('击杀排行榜', 6, gui => {
const info = layoutChest(gui, list.map((e, i) => ({
icon: i === 0 ? 'minecraft:golden_helmet' : 'minecraft:player_head',
name: `#${i + 1} ${e.name} — ${e.kills} 杀`
})), { rows: 6, page: page, reserve: 1, topMargin: 0 })
const bottom = 5
if (info.page > 0) gui.button(0, bottom, 'minecraft:arrow', '« 上一页', () => {})
if (info.page < info.maxPage - 1) gui.button(8, bottom, 'minecraft:arrow', '下一页 »', () => {})
gui.button(4, bottom, 'minecraft:paper', `第 ${info.page + 1} / ${info.maxPage} 页`, () => {})
})
})
9.6 案例 D:写入的 4 个实用细节¶
// ① 别把「物品对象」直接塞进 JSON —— 只存 id 和数量
// ❌ 猪:{ item: Item.of('minecraft:diamond') } → 序列化出来一堆你不想看的东西
// ✅ 好:{ id: 'minecraft:diamond', count: 1 }
let data = { id: String(stack.id), count: stack.count }
// ② 数值类型保持一致(JSON 里 1 和 "1" 是两回事,读回来比较会踩坑)
data.price = Number(data.price || 0)
// ③ 频繁写会卡服:节流 + 退出时补写
let dirty = false
PlayerEvents.loggedOut(event => { if (dirty) { flush(); dirty = false } })
// ④ 存之前先备份(改坏了还能救)
ServerEvents.loaded(event => {
try {
const f = JsonIO.read('kubejs/data/my_shop.json')
JsonIO.write('kubejs/data/my_shop.bak.json', JsonIO.of(f))
} catch (e) { /* 首次运行没有文件,正常 */ }
})
9.7 JSON × Chest GUI 的最常见组合套路¶
// 数据在 JSON,界面只负责"显示 + 改" → 改完立刻写回
ItemEvents.rightClicked('kubejs:admin_wand', event => {
const p = event.player
const cfg = JsonIO.read('kubejs/data/shop_catalog.json') // ① 读
p.openChestGUI('管理:价目表', 6, gui => {
layoutChest(gui, cfg.items.map((g, idx) => ({
icon: g.id,
name: `${g.name} · ${g.price} 币(左键 +10 / 右键 -10)`,
onClick: click => {
// ② 改(用 idx 定位到 JSON 里的那条)
if (click.button === 1) g.price = Math.max(0, g.price - 10)
else g.price += 10
// ③ 写(立刻落盘)
JsonIO.write('kubejs/data/shop_catalog.json', JsonIO.of(cfg))
click.slot.gui.player.tell(`§a${g.name} 新价格:${g.price}`)
// ④ 重开界面刷新显示(同第 7 节的翻页思路)
p.closeContainer()
}
})), { rows: 6, reserve: 1 })
})
})
💡 一句话总结:
JsonIO.read拿数据 → 用普通 JS 对象改 →JsonIO.write(path, JsonIO.of(数据))落盘; 界面要「刷新」就重新openChestGUI(翻页/改价都是这个套路)。
10. 速查表¶
| 想做什么 | 写法 |
|---|---|
| 打开自定义箱子界面 | player.openChestGUI(标题, 行数1~6, gui => {...}) |
| 放物品到格子 | gui.getSlot(x, y).setItem('id') 或 gui.getSlot(x, y).setItem('2x id') |
| 读格子物品 | gui.getSlot(x, y).getItem() |
| 放一个按钮 | gui.button(x, y, 物品, 名称, 点击回调) |
| 左键点击 | slot.setLeftClicked(cb) |
| 右键点击 | slot.setRightClicked(cb) |
| Shift+左键 | slot.setShiftLeftClicked(cb) |
| 中键点击 | slot.setMiddleClicked(cb) |
| 双击 | slot.setDoubleClicked(cb) |
| 批量配置区域 | gui.slot(x0, y0, x1, y1, slot => {...}) |
| 任意格子点击 | gui.anyClicked = click => {...} |
| 点背包格子 | gui.inventoryClicked = click => {...} |
| 关闭界面 | gui.closed = () => {...} |
| 阻止默认行为 | click.setHandled() |
| 打开容器界面 | player.openInventoryGUI(容器, 标题) |
| 自由容器菜单 | player.openGUI(gui => { gui.title/width/height = ... }) |
| 点击事件里拿玩家 | click.slot.gui.player |
| 翻页 | 页码存变量 → 点按钮改页码 → 重新 openChestGUI 重画(第 7 节) |
| 自动排版 | layoutChest(gui, items, {rows, page, center, reserve})(第 8 节) |
| 读 JSON | JsonIO.read('kubejs/data/xxx.json') → JS 对象(第 9 节) |
| 写 JSON | JsonIO.write('kubejs/data/xxx.json', JsonIO.of(对象)) |
| 对象 ↔ JSON 字符串 | JsonIO.toString(值) / JsonIO.parse(字符串) |
💡 记忆口诀:
openChestGUI(标题, 行数, 回调)开界面 →gui.button(x, y, 物品, 名, 回调)放可点按钮 →gui.getSlot(x, y).setItem(...)摆物品 →slot.setLeftClicked/setRightClicked/...绑点击 →gui.closed收尾;点击回调里用click.slot.gui.player拿玩家。 💡 进阶三件套:排版用layoutChest()(自动居中/留边距)→ 翻页用「改页码 + 重开界面」→ 存档用JsonIO.read/write(读回来就是普通 JS 对象,直接用点号改)。
本文为 KubeJS 1.20.1 中文文档原创篇目(2026-09-25 增补:翻页分页 / 自动排版 layoutChest / JsonIO 存数据 4 个案例)。API 依据 KubeJS 6.1 源码 dev.latvian.mods.kubejs.gui(KubeJSGUI / KubeJSMenu / CustomChestMenu / ChestMenuData / ChestMenuSlot),1.20.1 内置无需插件。