本篇教程由作者设定未经允许禁止转载。

使用KubeJS进行魔改的开发者们可能会在学习中发现,KubeJS本身并没有很好的GUI支持,这使得很多奇思妙想都难以实现,但MBD2能够部分补齐这块短板,即使一切都需要一个机器方块作为基础(当然在魔改中你可以将制造机器方块作为解锁系统的条件)

(!注:本教程并未考虑提升可读性和拓展性而创建函数来完成对应功能,如果你学会了,可以自行重构代码来保持较高的可读性并增加可拓展性。)

第一步:创建MBD2机器

首先我们需要使用MBD2创建一个机器,并在添加好需要的特性后完成GUI的编辑,本教程并非MBD2使用教学所以仅仅简单提及。
[1.20.1]基于MBD2和KubeJS的可视化自定义系统-第1张图片

第二步:确定使用事件

在完成机器的创建之后,我们进入到通过KubeJS编写GUI逻辑的阶段:

//server_scripts
let machineOpenLoop = {}
MBDMachineEvents.onOpenUI('mbd2:new_machine1',e=>{
    let event = e.getEvent()
})
PlayerEvents.inventoryClosed(event=>{
})

以上是我们完成GUI控制所必需的两个事件,第一个是MBD2所自带的打开UI事件,会在你打开机器GUI时触发一次。而第二个事件是KubeJS所提供的事件,会在所有inventory关闭的时候触发(包括关闭机器GUI、箱子等),而在本教程中我们希望它能够在玩家关闭GUI之后能够停止对应的功能检测。

machineOpenLoop 变量可能会让你感到疑惑,但别心急,在之后你会知道它具有非常重要的作用。

第三步:对机器进行监听

因为上述两个事件皆只会触发一次,所以我们需要通过scheduleRepeatingInTicks来持续监听机器。

MBDMachineEvents.onOpenUI('mbd2:new_machine1',e=>{
    let event = e.getEvent()
    machineOpenLoop[event.player.id] = event.player.server.scheduleRepeatingInTicks(1,()=>{
    })
})

在这里,我们为machineOpenLoop赋值,由于这是一个对象,所以我们能够通过这种形式来为它进行赋值。 对比常规的 

let loop = event.player.server.scheduleRepeatingInTicks(1,()=>{})

machineOpenLoop[event.player.id]和在一方面能够和loop有相同功能的同时,还做出了对 多人游戏 的兼容


server script的本质

许多只专注于魔改的开发者可能会产生一种误区:server script和client script除了有部分方法之外没多大区别。
其实两者的区分在名字上便体现出来了,即:server script是在server 服务端运行的脚本,而client script是在client 客户端运行的脚本。
在单人游戏测试中,Server和Client都被你本地运行,所以你的server和client可以互相获取脚本。
但在多人游戏中,作为玩家是只有客户端在运行的,所以在客户端中是无法获取服务端所运行的脚本的,而服务端同理。
而对于写在事件外的变量,machineOpenLoop是仅仅在一个服务端上运行的,任何玩家触发了对应更改machineOpenLoop内容的操作都会导致唯一的这一个变量产生改变。
所以如果我们要用该变量来判断玩家是否在GUI中并且控制监听的开启和关闭,必须区分出不同玩家。而用对象这种带有key value键值对的数据结构是非常不错且简单的选择:
玩家A只会让machineOpenLoop[玩家A]的value改变,玩家B只会让machineOpenLoop[玩家B]的value改变。当然你也可以不用id而是用uuid来作区分,一切都取决于你。


好了,在我们创建好这个基础的循环检测之后,我们需要为其添加一个停止检测的条件。

PlayerEvents.inventoryClosed(event=>{
    if (machineOpenLoop[event.player.id] == undefined) {
        return
    }
    machineOpenLoop[event.player.id].clear()
    machineOpenLoop[event.player.id] = undefined
})

我们用一个if判断玩家关闭的是否关闭的是对应机器GUI,显然可见:当玩家没有打开机器GUI时,machineOpenLoop[event.player.id]的内容显然是还未被定义的,所以直接被转出,而不再执行下列的代码。而当machineOpenLoop[event.player.id]已被定义不满足转出条件时,显然代表玩家在GUI界面(因为只有打开机器GUI事件才会完成定义),于是执行下列代码。

machineOpenLoop[event.player.id].clear()清除了循环检测的loop,并在之后将对应内容重新赋值为未定义状态。

至此,一个机器检测循环初步建立。

第四步:编写实际功能

作为教程,我尽量只写一些简单的功能,所以请记住:通过此法能够做到的事是非常多的,你的想象力和代码编写水平才是制约你的因素。

现在,我希望编写一个给武器注入液体来让武器获得提升的功能。

首先我需要保持对于机器物品槽位和液体槽位的检测,所以写下了如下的代码

MBDMachineEvents.onOpenUI('mbd2:new_machine1',e=>{
    let event = e.getEvent()
    machineOpenLoop[event.player.id] = event.player.server.scheduleRepeatingInTicks(1,()=>{
        let inv = event.machine.getCapability(ForgeCapabilities.ITEM_HANDLER).orElse(null)
        let fluid_tank = event.machine.getCapability(ForgeCapabilities.FLUID_HANDLER).orElse(null)
        let slot = inv.getStackInSlot(0)
        let fluid = fluid_tank.getFluidInTank(0)
    })
})


现在我们有了 slot fluid 两个变量来检测机器内所有的物品和液体。

由此我们可以开始愉悦地if嵌套:

        let fluid_type = fluid.getFluid().getFluidType()
        if (slot.hasTag('minecraft:tools')) {
            if (fluid_type == 'minecraft:water' && fluid.amount >= 500) {
                
            }
            if (fluid_type == 'minecraft:lava' && fluid.amount >= 500) {
                
            }
        }

从代码中我们可以了解到我希望slot是工具且在机器内部有大于等于500mb对应液体的时候才会满足条件进入对应的if结果执行

我希望能够花费500mb的水来让工具获取效率3附魔,大于等于效率三的物品则不会进行操作,我们可以有

if (fluid_type == 'minecraft:water' && fluid.amount >= 500) {
                if (slot.enchantments.get("minecraft:efficiency") < 3) {
                    slot = slot.enchant("minecraft:efficiency",3)
                    //这是MBD2的清除液体方法
                    fluid_tank.drain(Fluid.of('minecraft:water',500),"execute")
                    //我们需要设置inventory中指定槽位的物品为更新后的物品来完成物品替换
                    inv.setStackInSlot(0,slot)
                }
            }

同样的,我希望在注入岩浆后可以给物品添加上火焰附加2效果,可以在对应的地方写上:

slot = slot.enchant("minecraft:fire_aspect",2)

                    fluid_tank.drain(Fluid.of('minecraft:lava',500),"execute")

                    inv.setStackInSlot(0,slot)

现在我们的OpenUI事件代码看起来是这样的:

MBDMachineEvents.onOpenUI('mbd2:new_machine1',e=>{
    let event = e.getEvent()
    machineOpenLoop[event.player.id] = event.player.server.scheduleRepeatingInTicks(1,()=>{
        let inv = event.machine.getCapability(ForgeCapabilities.ITEM_HANDLER).orElse(null)
        let fluid_tank = event.machine.getCapability(ForgeCapabilities.FLUID_HANDLER).orElse(null)
        let slot = inv.getStackInSlot(0)
        let fluid = fluid_tank.getFluidInTank(0)
        let fluid_type = fluid.getFluid().getFluidType()
        if (slot.hasTag('minecraft:tools')) {
            if (fluid_type == 'minecraft:water' && fluid.amount >= 500) {
                if (slot.enchantments.get("minecraft:efficiency") < 3) {
                    slot = slot.enchant("minecraft:efficiency",3)
                    fluid_tank.drain(Fluid.of('minecraft:water',500),"execute")
                    inv.setStackInSlot(0,slot)
                }
            }
            if (fluid_type == 'minecraft:lava' && fluid.amount >= 500) {
                if (slot.enchantments.get("minecraft:fire_aspect") < 2) {
                    slot = slot.enchant("minecraft:fire_aspect",2)
                    fluid_tank.drain(Fluid.of('minecraft:lava',500),"execute")
                    inv.setStackInSlot(0,slot)
                }
            }
        }
    })
})

但是光是这样的效果是否过于无趣呢?既然能为物品注入液体,那是否液体也会在物品内部发生反应?——如此有趣,让我们为武器添加上能够根据液体进行反应的部分

功能是:物品同时含有水和岩浆时,会生成黑曜石并清除内部的水和岩浆,在同时获得水和岩浆带来的提升同时再获取黑曜石给予的耐久附魔

让我们先回到注入水给予效率3效果的片段,并增加部分代码

if (fluid_type == 'minecraft:water' && fluid.amount >= 500) {
                if (slot.enchantments.get("minecraft:efficiency") < 3) {
                    let fluidNbt = slot.nbt.get("involved_fluid")
                    if (fluidNbt == null) {
                        fluidNbt = []
                        slot.nbt.merge({involved_fluid:fluidNbt})
                    }
                    //如果物品已经含有岩浆,岩浆和水会产生黑曜石
                    if (fluidNbt.toString().includes('minecraft:lava')) {
                        fluidNbt = fluidNbt.filter(e=>e != 'minecraft:lava')
                        fluidNbt.push('minecraft:obsidian')
                        if (slot.enchantments.get("minecraft:unbreaking") < 3) {
                            slot = slot.enchant("minecraft:unbreaking",3)
                        }
                    }
                    else{
                        fluidNbt.push('minecraft:water')
                    }
                    slot.nbt.merge({involved_fluid:fluidNbt})
                    
                    slot = slot.enchant("minecraft:efficiency",3)
                    fluid_tank.drain(Fluid.of('minecraft:water',500),"execute")
                    inv.setStackInSlot(0,slot)
                }
            }

可以发现,我们在注入液体完成效果前对物品的nbt进行了部分操作:包括为不存在对应nbt的物品添加nbt、根据已经内含的液体改变nbt、为结果添加新的nbt和附魔。

而在岩浆的部分,我们同样添加上对应的内容

if (fluid_type == 'minecraft:lava' && fluid.amount >= 500) {
                if (slot.enchantments.get("minecraft:fire_aspect") < 2) {
                    let fluidNbt = slot.nbt.get("involved_fluid")
                    if (fluidNbt == null) {
                        fluidNbt = []
                        slot.nbt.merge({involved_fluid:fluidNbt})
                    }
                    //如果物品已经含有水,岩浆和水会产生黑曜石
                    if (fluidNbt.toString().includes('minecraft:water')) {
                        fluidNbt = fluidNbt.filter(e=>e != 'minecraft:water')
                        fluidNbt.push('minecraft:obsidian')
                        if (slot.enchantments.get("minecraft:unbreaking") < 3) {
                            slot = slot.enchant("minecraft:unbreaking",3)
                        }
                    }
                    else{
                        fluidNbt.push('minecraft:lava')
                    }
                    slot.nbt.merge({involved_fluid:fluidNbt})
                    slot = slot.enchant("minecraft:fire_aspect",2)
                    fluid_tank.drain(Fluid.of('minecraft:lava',500),"execute")
                    inv.setStackInSlot(0,slot)
                }
            }

到这里,基本的功能已经算完成了,但是岂止于此,我们可以在GUI中添加Painter对象来让GUI更具活力!

为GUI添加Painter对象

关于Painter的教程本文并不会具体阐述,如有需要可以查阅wiki:https://wiki.latvian.dev/books/kubejs-legacy/page/painter-api

本文暂不提及动态创建Painter对象的教程,如有需要也许会单独编写一个教程。


这里我想要显示物品包含的液体图片,为了完成该功能,我们需要在

if (slot.hasTag('minecraft:tools'))这一if的结果内进行代码的编写:

        let fluidNbt = slot.nbt.get("involved_fluid")
        let fluidTex = []

因为fluidNbt其实是在更加内层定义的变量,其作用域只在那层,所以我们在这需要重新定义一次fluidNbt变量。为了控制对应液体展示的贴图文件,我们定义一个fluidTex的变量来将液体转换为对应贴图文件

为了完成这一转接,我们用一个for循环:

for (let i = 0; i < 3; i++) {
            let element = fluidNbt[i];
            switch (element) {
                case 'minecraft:lava':
                    fluidTex[i] = ("minecraft:block/lava_flow")
                    break;
                case 'minecraft:water':
                    fluidTex[i] = ("minecraft:block/water_flow")
                    break;
                case 'minecraft:obsidian':
                    fluidTex[i] = ("minecraft:block/obsidian")
                    break;
                default:
                    fluidTex[i] = ("minecraft:block/glass")
                    break;
            }
        }

注:这里我暂时只考虑3个painter对象(也就是3格液体显示),所以for循环直接使用了i < 3,如果要拓展液体显示的数量,可以根据需求进行更改。 而输出的fluidTex的每一个元素都对应了一个贴图路径。

在这之后我们开始构造Painter对象:

event.player.paint({
            FirstObj:{
                type: "atlas_texture",
                texture:fluidTex[0],
                x:-18,y:-72,z:0,
                alignX:'center',alignY:'center',
                w:18,h:18,draw: 'gui'
                },
            SecondObj:{
                type: "atlas_texture",
                texture:fluidTex[1],
                x:0,y:-72,z:0,
                alignX:'center',alignY:'center',
                w:20,h:20,draw: 'gui'
                },
            ThirdObj:{
                type: "atlas_texture",
                texture:fluidTex[2],
                x:18,y:-72,z:0,
                alignX:'center',alignY:'center',
                w:20,h:20,draw: 'gui'
                },
        })
        }else{
            event.player.paint({FirstObj:{type:"atlas_texture",remove:true},
                SecondObj:{type:"atlas_texture",remove:true},
                ThirdObj:{type:"atlas_texture",remove:true}})
        }
    })

因为只考虑3个液体,所以painter对象我只创建了3个,如果你想显示更多,请构造更多的painter对象。

而在构造好painter对象之后,我们需要知道这些painter对象在什么时候不应该显示:

根据功能,很显然在 玩家关闭GUI 和 物品槽位中并没有能够注入液体的物品 两个时候不该显示painter对象。

所以我们需要同时在上述两个地方编写移除painter对象的代码,而移除painter对象的代码主体为:

event.player.paint({FirstObj:{type:"atlas_texture",remove:true},
                SecondObj:{type:"atlas_texture",remove:true},
                ThirdObj:{type:"atlas_texture",remove:true}})

注:移除painter对象需要声明其type,否则会在log中报“Unknown Painter Type”的错误。

完成之后,我们的显示效果是这样的[1.20.1]基于MBD2和KubeJS的可视化自定义系统-第2张图片

[1.20.1]基于MBD2和KubeJS的可视化自定义系统-第3张图片

完整代码

let machineOpenLoop = {}
MBDMachineEvents.onOpenUI('mbd2:new_machine1',e=>{
    let event = e.getEvent()
    machineOpenLoop[event.player.id] = event.player.server.scheduleRepeatingInTicks(1,()=>{
        let inv = event.machine.getCapability(ForgeCapabilities.ITEM_HANDLER).orElse(null)
        let fluid_tank = event.machine.getCapability(ForgeCapabilities.FLUID_HANDLER).orElse(null)
        let slot = inv.getStackInSlot(0)
        let fluid = fluid_tank.getFluidInTank(0)
        let fluid_type = fluid.getFluid().getFluidType()
        if (slot.hasTag('minecraft:tools')) {
            if (fluid_type == 'minecraft:water' && fluid.amount >= 500) {
                if (slot.enchantments.get("minecraft:efficiency") < 3) {
                    let fluidNbt = slot.nbt.get("involved_fluid")
                    if (fluidNbt == null) {
                        fluidNbt = []
                        slot.nbt.merge({involved_fluid:fluidNbt})
                    }
                    //如果物品已经含有岩浆,岩浆和水会产生黑曜石
                    if (fluidNbt.toString().includes('minecraft:lava')) {
                        fluidNbt = fluidNbt.filter(e=>e != 'minecraft:lava')
                        fluidNbt.push('minecraft:obsidian')
                        if (slot.enchantments.get("minecraft:unbreaking") < 3) {
                            slot = slot.enchant("minecraft:unbreaking",3)
                        }
                    }
                    else{
                        fluidNbt.push('minecraft:water')
                    }
                    slot.nbt.merge({involved_fluid:fluidNbt})
                    
                    slot = slot.enchant("minecraft:efficiency",3)
                    fluid_tank.drain(Fluid.of('minecraft:water',500),"execute")
                    inv.setStackInSlot(0,slot)
                }
            }
            if (fluid_type == 'minecraft:lava' && fluid.amount >= 500) {
                if (slot.enchantments.get("minecraft:fire_aspect") < 2) {
                    let fluidNbt = slot.nbt.get("involved_fluid")
                    if (fluidNbt == null) {
                        fluidNbt = []
                        slot.nbt.merge({involved_fluid:fluidNbt})
                    }
                    //如果物品已经含有水,岩浆和水会产生黑曜石
                    if (fluidNbt.toString().includes('minecraft:water')) {
                        fluidNbt = fluidNbt.filter(e=>e != 'minecraft:water')
                        fluidNbt.push('minecraft:obsidian')
                        if (slot.enchantments.get("minecraft:unbreaking") < 3) {
                            slot = slot.enchant("minecraft:unbreaking",3)
                        }
                    }
                    else{
                        fluidNbt.push('minecraft:lava')
                    }
                    slot.nbt.merge({involved_fluid:fluidNbt})

                    slot = slot.enchant("minecraft:fire_aspect",2)
                    fluid_tank.drain(Fluid.of('minecraft:lava',500),"execute")
                    inv.setStackInSlot(0,slot)
                }
            }
        let fluidNbt = slot.nbt.get("involved_fluid")
        let fluidTex = []

        for (let i = 0; i < 3; i++) {
            let element = fluidNbt[i];
            switch (element) {
                case 'minecraft:lava':
                    fluidTex[i] = ("minecraft:block/lava_flow")
                    break;
                case 'minecraft:water':
                    fluidTex[i] = ("minecraft:block/water_flow")
                    break;
                case 'minecraft:obsidian':
                    fluidTex[i] = ("minecraft:block/obsidian")
                    break;
                default:
                    fluidTex[i] = ("minecraft:block/glass")
                    break;
            }
        }

        //可视化painter
        event.player.paint({
            FirstObj:{
                type: "atlas_texture",
                texture:fluidTex[0],
                x:-18,y:-72,z:0,
                alignX:'center',alignY:'center',
                w:18,h:18,draw: 'gui'
                },
            SecondObj:{
                type: "atlas_texture",
                texture:fluidTex[1],
                x:0,y:-72,z:0,
                alignX:'center',alignY:'center',
                w:20,h:20,draw: 'gui'
                },
            ThirdObj:{
                type: "atlas_texture",
                texture:fluidTex[2],
                x:18,y:-72,z:0,
                alignX:'center',alignY:'center',
                w:20,h:20,draw: 'gui'
                },
        })
        }else{
            event.player.paint({FirstObj:{type:"atlas_texture",remove:true},
                SecondObj:{type:"atlas_texture",remove:true},
                ThirdObj:{type:"atlas_texture",remove:true}})
        }
    })
})
PlayerEvents.inventoryClosed(event=>{
    if (machineOpenLoop[event.player.id] == undefined) {
        return
    }
    machineOpenLoop[event.player.id].clear()
    machineOpenLoop[event.player.id] = undefined
    event.player.paint({FirstObj:{type:"atlas_texture",remove:true},
        SecondObj:{type:"atlas_texture",remove:true},
        ThirdObj:{type:"atlas_texture",remove:true}})
})

基于这一方法的更复杂系统的实现预览

大体还原药剂工艺的炼金系统

[1.20.1]基于MBD2和KubeJS的可视化自定义系统-第4张图片