本篇教程由作者设定未经允许禁止转载。
使用KubeJS进行魔改的开发者们可能会在学习中发现,KubeJS本身并没有很好的GUI支持,这使得很多奇思妙想都难以实现,但MBD2能够部分补齐这块短板,即使一切都需要一个机器方块作为基础(当然在魔改中你可以将制造机器方块作为解锁系统的条件)
(!注:本教程并未考虑提升可读性和拓展性而创建函数来完成对应功能,如果你学会了,可以自行重构代码来保持较高的可读性并增加可拓展性。)
第一步:创建MBD2机器
首先我们需要使用MBD2创建一个机器,并在添加好需要的特性后完成GUI的编辑,本教程并非MBD2使用教学所以仅仅简单提及。
第二步:确定使用事件
在完成机器的创建之后,我们进入到通过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”的错误。
完成之后,我们的显示效果是这样的
完整代码
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}})
})
基于这一方法的更复杂系统的实现预览
大体还原药剂工艺的炼金系统