本篇教程由作者设定使用 CC BY-NC-SA 协议。
· 前言:
在MC1.12.2版本下进行匠魂2的词条(也叫特性,英文trait,下文将不会对这两个词作区分)的自定义需要依赖CrT和CoT两个基础模组(以及其他各种拓展模组诸如CraftTweaker Utils、RandomTweaker、CTIG、Zen Utils等更加方便地提供接口实现功能),而其核心功能特性效果的创建与编写则属于CrT的高级应用范畴——事件编写。这一部分内容拥有很高的门槛,属于是“教的学不会,会的不用教”。笔者粗略翻看了各种CrT事件教程和匠魂的自定义词条教程,发现普遍都讲得不够“贴近萌新”,特别是参数获取这一块,很多时候萌新根本不知道“怎样达成目的”,看了大佬的教程代码也会一脸懵:“这是怎样做到的?”,甚至初学者学到一定程度后仍然会对各种类缺乏明确的认识理解。为了解决这个问题,在本篇教程里,笔者会在讲述如何自定义匠魂词条之前,根据自己浅薄的理解,尽可能细地讲述CrT“相对进阶”的知识,减少萌新的疑虑,弥补这一部分的空白。
为了满足魔改需求,CrT提供了一门名为“ZenScript”的编程语言方便玩家编写代码修改游戏的各种内容,包括各类合成配方的修改、自定义物品(以及属性、作用等,需要依赖CoT)、创建事件等,其代码文件以.zs文件形式储存。本教程会从ZenScript的基础重要知识讲解开始,以几个词条的构建编写过程为例,梳理从零开始编写自定义匠魂词条的思路,教会玩家自定义匠魂词条的基本思想。笔者能力有限,教程可能会有很多缺陷与不足,还请各位读者指出,望大佬海涵!
注意:本教程不会讲述注册匠魂特性的颜色、名字、描述等基础部分,教程核心在于怎样在游戏中实现自定义的效果,其余部分请参考其他教程。
本教程遵循CC: BY-NC-SA共享协议,所有代码均禁止商用!!!
· 写给萌新的话大佬请忽略
首先,欢迎各位怀抱着各种脑洞、想要跃跃欲试将其实现并放入自己游戏的新手读者们打开这篇教程,我是本篇教程的笔者JackStuart(昵称JS)。本篇教程将会由我引导大家怎样从零开始踏上自定义词条之旅、从初识CrT的ZenScript语言到灵活运用,最 后创建出属于自己的特性。由于ZenScript本质上也属于一门编程语言,如果说玩家能有最基础的编程语言基础(能读懂最基础的代码语句,知道变量、基础运算、循环数组以及函数,最好能知道类与对象是什么),并且知道ZenScript的相关基础语法(如局部变量、基础运算、循环、数组、函数以及基本类中的基础数据类型等)那当然是再好不过了。不过如果说是完全零基础的倒也无妨,可以选择ZenScript作为你的第一门编程语言(虽然这个语言实用性并不高就是了hhh)。关于ZenScript的基础语法部分教学,已经有很多dalao写了很多详细的教程,具体可以查看下边友谊妈的ZenScript新手教程链接和[基础向]的内容教程,本篇教程将不会再细讲这部分内容。将ZenScript的基础部分吃透后,便可以正式食用本篇教程了哦~。
这里补充一点:由于VS Code有几个拓展支持ZenScript语法高亮显示,这里强烈建议使用VS Code编写zs脚本。JS使用的是如下的三个拓展,这里也推荐各位安装使用:
〇、有用的链接
开头先贴一些常用的链接,把它们全部收藏起来,你会用到的:
· CrT官方wiki(几乎没有中文翻译,不过强烈建议阅读原文。这个文档类似于工具书,最好别期望你能用它从零开始轻松上手ZenScript): Click Here
· 友谊妈写的ZenScript新手教程:Click Here 第二个链接:Click Here(推荐这个,访问速度快)
· CrT的高级运用:Click Here
社区里几个比较好的的教程推荐:(排名不分先后,建议综合食用)
[CrT/ZenScript基础内容-基础向]
· 魔改基础-Crafttweaker-1.12.2-过关基础知识点(By shinra):Click Here
· 1.12.2crt——从入门到精通(By 小太阳):Click Here
· 从〇开始的编程逻辑(社区教程):Click Here
[事件编写-进阶向](参考一下大佬们是怎样写事件代码的)
· 关于crafttweaker events对于萌新的入门教程(By Luck_Hot_Dog):Click Here
· 1.12.2使用CrT对游戏事件的操作(By Biggest_Xuan):Click Here
· 真正的自定义机器:事件(By Doremy):Click Here 【警告:示例极度硬核!】
· 开天辟地 —— CrT事件教学(By Doremy):Click Here 【警告:示例极度硬核!】
......(好吧我懒得写了,CrT教程区还有很多优秀的教程可以翻一翻)
[CoT/匠魂区的自定义特性教程](看完这个教程,这些应该不算难)
· 使用Cot构建匠魂新特性(词条)(By 卡卡西):Click Here
· 使用CoT为匠魂添加自定义特性简单示例(By Nishiki):Click Here
· cot为匠魂添加稍微复杂一点的特性事例(By 唐轩宇):Click Here
一、“相对进阶”的ZenScript的基础知识
俗话说,磨刀不误砍柴工。这一部分JS会在ZenScript的基础内容上,介绍一些“相对进阶”的ZenScript基础知识,如类与对象、常用类、ZenGetter/Setter/Method和几个关键词,为后续内容作铺垫。
1.类与对象
学过面向对象语言的玩家对此一定不陌生。而对于不怎么了解这两个术语的初学者而言,我会简单介绍这两个术语的意思并且着重介绍ZenScript中的“类”(对于有编程基础的读者可以直接跳到string类一行):可以简单理解成“类”就是自定义变量过程中你定义变量的“类型”,而“对象”则是这个过程中的那个“变量”。以C语言的狗都能读得懂的(简单语法举例:
int i;
这里我们定义了一个int类型的变量i。在这个过程中,我们告诉系统:我需要自定义一个变量叫i,这个变量是整数int类型的。这里的int就是“整数类型”这个类的名字,而i就是这个类下的一个对象。如果我们把这个语句用ZenScript的语法表示那它是这样的:
var i as int ; //var+变量名+as+类型名 格式定义某种类型的变量
//注意这个格式不仅限于基础类,各种类都可以这样定义,如下文会提及的IEntityLivingBase类:
var entity as IEntityLivingBase;
像int这样的类是最最基础的类的一种,作用就是直接存储一种基础数据类型的一个值。除了int以外,在ZenScript里还有布尔型(bool)、长整型(long)、单精度浮点数(float)、双精度浮点数(double)和无类型(void),包括int总共6种存储数据的类。这6种类并没有任何所谓的ZenGetter、ZenSetter和ZenMethod(这三个名词下文会详细介绍),属于ZenScript的基本类,底层上不依赖minecraft,因此可以直接使用关键词。
注意:bool类型仅有true和false两个值,不能用1和0代替;void类型常用于函数表示函数无返回值。
除了以上6种基础类外,还有一种基础类:字符串(string),它用于储存文本。在这里额外插入一个知识点:ZenScript中的string类不仅兼容java中对string类的操作方法,还能直接用“==”判断两个string类对象是否相等。
类的作用可以简单理解为造了一个黑盒子用于储存各种数据并且给这个盒子贴上一个标签命名,这样我可以方便地对数据进行归类操作;我还能给这个黑盒子挖几个“洞”用于操作里边的数据(这里的“洞”就是所谓“类的接口”);黑盒子还能套黑盒子用于存放其他黑盒子类型的数据;黑盒子还能被改装、拓展其他功能从而派生出其他的黑盒子(“派生类”)......然而说了这么多,实际上在使用ZenScript的时候很少需要玩家直接创建类,更多的是需要知道各种类的作用(它们在MC里代表了什么?)以及知道怎样调用类的各种接口(ZenGetter、ZenSetter和ZenMethod)。接下来我会先解决第一个问题,介绍一些教程中所需的、ZenScript的常用类。
2.ZenScript中的常用类
这一节介绍教程中所需的、ZenScript中的常用类,它们直接和MC的具体游戏内容挂钩了。必须要注意的一点是,这些类依赖于MC的底层代码,需要声明该类的对象时需要“导包”,也即导入相关的类定义文件,比如我定义一个叫testItem的IItemStack类变量:
import crafttweaker.item.IItemStack; //“导包”,不然系统不知道你的“IItemStack”是一个类的名字!
var testItem as IItemStack;//定义一个叫testItem的IItemStack类变量
导包的目的是防止系统无法识别使用的类导致代码出错,作者在每一个类的开始部分加了导包建议(原文:It might be required for you to import the package if you encounter any issues (like casting an array), so better be safe than sorry and add the import.),初学者建议在熟练掌握导包规律之前最好“用了什么类型的数据就导什么包”。
实际上根据笔者的经验,只需要对那些用使用非基本类类名作为关键词的非基本类进行导包即可(初学者强烈建议别这样做!!!)
下边是笔者认为的、教程中所需的、ZenScript中的常用类:
(1)IItemStack——表示MC里确确实实的一个具体的物品(堆)。它表示了MC中一个占据了单个物品栏格子的确定物品(们),可以是实际存在于游戏中的一些物品,比如说玩家物品栏里的一把钻石剑、各种容器里的两块铁锭、怪物背包里的一顶金头盔乃至掉落物实体所包含的三枚钻石,也能是配方中所需要的、带有某个或某些NBT标签的、并不一定实际存在的物品(堆)。IItemStack包含了“这个物品(堆)是啥”(属于啥)、“它叫啥?”(显示的名字)、“有多少数量?”、“最大耐久度是多少?”、“有啥附魔?”、“有什么NBT?”、“meta值是多少”等具体信息数据。举个例子:
· 我把一组零6个苹果(林檎)分成三堆,每堆的数量分别是11个、45个、14个,这三堆苹果占据了我三个物品栏。在ZenScript看来,这三堆苹果每堆都是一个IItemStack(对象);
· 我把一把满配的钻石剑用铁砧敲了个“Excalibur”的名字并扔给了一个叫做“King Arthur”的僵尸,这把钻石剑占据了僵尸的一个主手物品栏。在ZenScript看来,这把钻石剑就是一个IItemStack,包含了“它是一把钻石剑”、“它的名字是‘Excalibur’”、“它拥有满配剑所有的附魔NBT”(工具的附魔以NBT形式存储在具体物品上)等信息;
· 我用铁砧把饰品“钴盾牌”和“黑曜石头颅”敲在一起变成了“黑曜石盾牌”,在这个配方中“钴盾牌”、“黑曜石头颅”和“黑曜石盾牌”都是一个IItemStack;
· 我和一个名叫“Liben”的旅行商人做交易,我用5个苹果、5个生鸡肉换了40枚钻石和一组附魔瓶。这个(旅行商人交易)配方中“5个苹果”、“5个生鸡肉”、“40枚钻石”以及“一组附魔瓶”都是一个IItemStack;
(2)IItemDefinition——表示MC中“一种物品”,带有该类物品基础的不变属性数据信息。它并非像IItemStack一样表示某个具体的物品而是表示“一类物品”,这类物品都有一个共同的id、(非本地化的)物品名以及共同的基础属性,其本地化名是玩家称呼该种物品具体单个物体时的中心词。举例说明:
· 一个1点耐久的木斧头、一个10点耐久的顶配时运亡灵杀手的木斧头、一个满耐久,带有“不可破坏”tag的,被命名为“&&……*%¥”的木斧头,这三个物品(IItemStack)都是“木斧头”(IItemDefinition),它们都能快速伐木、拥有至少9点的基础攻击力和0.9的基础攻击速度、扔进熔炉里能烧一个物品。这个“木斧头”类就是一个IItemDefinition;
· 上述例子中的那把“Excalibur”就属于“钻石剑”这个IItemDefinition。无论怎么对这把剑加再多的tag、再怎么消耗耐久,它都属于“钻石剑”这个IItemDefinition;
(3)IPlayer——表示MC中某一特定玩家,带有玩家id、名字、物品栏、位置等数据信息。比如笔者用“Jack_Stuart”这个id账号登入了MC并进入某一世界,那便有名字是“Jack_Stuart”的这个玩家(IPlayer)加入了该世界;笔者用该角色在游戏里吸引了某只僵尸的注意,那该僵尸的仇恨对象便是该玩家(IPlayer)。值得注意的是IPlayer是IEntityLivingBase的派生类,所有IEntityLivingBase的数据接口IPlayer都能直接调用。
(4)IEntityLivingBase——表示MC中某一特定的、“活着的”实体,带有该实体的生命值、速度、所处状态等数据信息。它可以是一只僵尸、一只苦力怕、一只羊、一个玩家甚至一条末影龙。值得注意的是IEntityLivingBase是IEntity的派生类,所有IEntity的数据接口IEntityLivingBase都能直接调用。
(5)IEntity——表示MC中某一特定的“实体”。它的范围囊括了上文的IEntityLivingBase,还包括掉落物、经验球、盔甲架、物品展示框、投掷物等“非生命的”实体,所包含数据信息也更加具体、基础。
(6)IWorld——表示MC中某一特定的“维度”,如下界(地狱)、末地以及暮色森林模组添加的暮色森林、以太模组添加的天堂等。它涵盖了维度id等信息,也可以通过它获取是否为晚上、月相、游戏总时长等状态信息(游戏内具体信息请移步IWorldProvider,包括游戏日的时间)。值得注意的是,下列语句经常用于事件的编写中使得事件仅在服务端加载:(自定义特性本质上也属于编写事件,所以这俩语句会很常用)
if( !world.remote ){
//your code
}
//如果是在函数中也有这种写法:
if(world.remote)return [请把这个方括号带着本体和里边的包含提示一起换成函数需要返回的数据类型] ;
//your code
(7)IPotion和IPotionEffect:类比IItemDefinition和IItemStack,前者表示(抽象的)“一种药水”而后者表示“一种具体的药水效果”。后者包含药水强度(amplifier)、持续时间等。玩家、生物身上获取的药水效果类型均为后者。
实际上,ZenScript还有非常多的类可供使用,具体可以查阅官方wiki查看其作用和接口(在左侧侧边栏的Vanilla一栏中展开查看),笔者这里不再赘述。(话说我是不是应该写一写官方wiki的正确打开方式()
3.函数
在JS邀请一个基本上没有啥编程基础的朋友学习了这篇教程后发现很有必要补充讲一讲啥是函数。
“函数”,不同于在数学上定义:“一种映射”,在计算机编程领域中则被解释为“一个具有某种功能的、可以被反复执行的代码段”。我们可以把函数比作一台机器,这台机器有两个功能,一个是“做了某件事”,另一个是“给你一个反馈信息”;它还有一个接口,以便于你往接口里塞数据信息,让机器知道“基于什么数据条件运行”。机器还有一个名字,让你知道你用的是“这台”机器而不是“那台”。现在我们把这个比喻翻译回代码语言,那“机器的功能”便是“函数的功能”;“做了某件事”则是函数内部代码具体运行的结果;“给你反馈信息”则是在代码层面、函数的“返回值”;机器的“接口”则被称为函数的“参数(表)”;机器的“名字”则是“函数名”。
对于函数,我们不仅要学习怎样使用它,我们还需要学习怎样编写它(特别是对于重复执行的代码段,用函数能够简化代码表达和编写流程)。现在我们具体来看几个具体的例子:
1.输入x的值,计算表达式“y=x^2”并且返回该表达式的值;
对于这台函数“机器”,它的“接口”便是你输入的x的值;它的“功能”则是计算表达式“y=x^2”,并且给你一个这个表达式的结果作为反馈信息。现在我们把这个函数命名为identifyPower。那对于这个函数,我们只需要放入x的值就可以获得x^2的值。在使用过程中我们无需关心这个函数是怎样运行的(哪怕它是从1+1开始算起),我们只用关心这个函数的功能,它所需要的数据、做的事和返回的数据,好比你不用关心你手上的科学计算器是怎么算正弦余弦函数而只要知道它能算三角函数的正余弦就够了。但是你不能要科学计算器计算非数字的的正余弦(比如说计算符号“+”的正余弦),换言之函数对其输入的数据的类型是有要求的,使用时我们必须要注意这一点。这里我们不妨定义identifyPower函数所需要的数据为float类型。
对于已经定义的函数,一般会给出这个函数的函数名、函数的参数(表)(包含所需参数个数以及其类型)、函数的作用和函数的返回值,并用适当的语言描述,例如上述identifyPower,我们可以将其描述为:float identifyPower( float x ):计算x的平方。函数名前的float表示函数的返回值类型(若是void则函数只会执行代码不会返回值);括号内部的float表示函数所需参数的类型,而x对应这个函数这个类型的一个变量名(注意在这里x只是一个符号而已,你可以把它替换为任意任何不会产生歧义的名字来表示)。函数的使用只需要往函数名后边括号中输入对应的参数即可。例子:
//这里假设我们定义了identifyPower这个函数,格式就是float identifyPower( float x )
var a as float = 42.0f;
var b as float = 0.0f;
b = identifiedPower(a) ; //直接将x替换为a即可,这个语句会先计算 identifyPower(a) ,后把它赋值给b
print(b);//日志里输出1764
接下来说说这个函数该怎么写,具体模板框架内容可以在这里查看:链接。在这里JS带着大家过一遍:
关键词function+函数名字+(参数列表)+as+返回值类型+{这里放你的代码,别忘了return对应的数据}
上述的函数可以写成:
function identifyPower(x as float) as float{
var y as int;
y = x * x;
return y;
};
值得注意的是,某些函数不需要参数,而某些函数不需要返回值;对于前者,括号以及其内的参数表可以省略不写,调用的时候直接使用函数名+()即可;对于后者,as+返回值类型可以省略,return处语句直接“return;”便可结束运行。如以下函数:
//不需要返回值的函数
function identifyPrint(yourString as string){
var outputString as string = yourString ~ "!";
print(outputString)
return;
}
//不需要函数参数的函数
function pi14() as double{
return 3.14159265358979;
}
//两者都不需要的函数
function checkPrint() {
print("Check!")
return;
}
var notAString as string = "This is actually a string";
identifyPrint(notAString);
checkPrint();
print(pi14());
//日志里自上而下输出的分别是:This is actually a string!、Check!和3.14159265358979
由于函数是一个“被包装过的代码段”,在除了输入数据输出计算结果外,实际我们还可以把一些操作放入函数中。这些操作可以是对某些数值做出调整,输出一些调试信息,也可以是把怪物血量减半、让玩家自己获得buff、调整世界时间等。这些操作特点是有实际的效果,但一般不会有返回数据的要求。如以下函数:给指定生物添加药水效果。
function giveEntityPotionEffect( target as IEntityLivingBase , potion as IPotion , duration as int , amplifier as int ){
target.addPotionEffect(potion.makePotionEffect(duration,amplifier));
}
实际使用只需要把参数扔进去就能达成效果了。
下文说的“ZenMethod”以及部分“ZenGetter”都属于函数,而且很多情况下它们都不会拥有参数表、返回值,其作用都是像上文说的“做了某件事”。
4.ZenGetter、ZenSetter和ZenMethod
简单来说,这三个都是ZenScript的数据接口。正如其名:ZenGetter的作用是获取类中的某个数据元素(一般用于获取相关数据以供后续操作),ZenSetter则是设置某个数据元素(一般用于执行某个操作达到魔改目的),而ZenMethod则偏向于提供一个特殊的数据接口用于处理特殊数据需求(多是用于执行某个操作达到魔改目的,少数用于获取数据)。这里以IEntityLivingBase类为例,查询CrT官方wiki可以知道这个类有这些接口:
如何使用这三种接口?我们假定一只僵尸(显然它属于IEntityLivingBase实体),在代码中明确定位到它的相关数据以后我们用一个自定义变量entity储存它的相关数据,然后使用这样的格式:
entity+“.”+ZenGetter/Setter/Method名(+后续的函数参数等)
来调用这三个接口,这里以health相关的ZenGetter、ZenSetter和两个常用的添加/移除药水效果的ZenMethod { void addPotionEffect(IPotionEffect potionEffect)和void removePotionEffect(IPotion potion) }为例:
import crafttweaker.entity.IEntityLivingBase; //使用类别忘记导包
var entity as IEntityLivingBase;
//假设我们已经获取了那只“僵尸”的信息并且储存在了entity这个变量里:
print(entity.health); //获取并输出僵尸的目前生命值(到crafttweaker.log)(在运行到这行代码时)
entity.health = 1.0f; //将僵尸的生命值设置为1.0 (小数后边加f/d表示float/double类型,如0.0d、0.0721f)(在运行到这行代码时)
entity.addPotionEffect(<potion:minecraft:glowing>.makePotionEffect(400,0)); // 调用entity.addPotionEffect(IPotionEffect potionEffect) 这个ZenMethod
//为僵尸身上添加“<potion:minecraft:glowing>.makePotionEffect(400,0)”这个IPotionEffect类型的(具体)药水效果(实际是20s、等级为1的发光效果)(在运行到这行代码时)
//注意,这个是个模板,可以直接套用
entity.removePotionEffect(<potion:minecraft:glowing>); // 调用entity.removePotionEffect(IPotion potion)这个ZenMethod
//移除僵尸身上的“<potion:minecraft:glowing>”这个IPotion类型的(某一种)药水效果(实际是原版的发光效果)(在运行到这行代码时)
在这里我写出以下事件代码以供展示效果:(只需要阅读有注释的部分即可)
//导包!导包!!导包!!!重要的事情强调三次
import crafttweaker.event.EntityLivingHurtEvent;
import crafttweaker.events.IEventManager;
import crafttweaker.entity.IEntityLivingBase;
import crafttweaker.player.IPlayer;
events.onEntityLivingHurt(function(event as EntityLivingHurtEvent){
var entity as IEntityLivingBase;//自定义一个IEntityLivingBase类型的、叫entity的变量
entity = event.entityLivingBase;//(通过事件的方式)获取目标僵尸(实际上可以是任意生物)的数据信息并储存在entity里
var source = event.damageSource;
if(!entity.world.remote){
if(!isNull(source.getTrueSource()) && source.getTrueSource() instanceof IPlayer){
var player as IPlayer = source.getTrueSource();
if(isNull(entity.getActivePotionEffect(<potion:minecraft:glowing>))){
//第一段代码
player.sendChat("ZenGetter测试,目标实体生命值:" ~ ( //使用player.sendChat(string message)这个代码在对话框内可视化输出(事实上这也是一个ZenMethod)
entity.health //这里是获取目标僵尸生命值的ZenGetter
)as string ); //将float类型转换成string类型以匹配代码参数,这是代码规范要求(具体操作下边会细说)
player.sendChat("ZenMethod测试,目标实体添加发光效果"); //可视化输出
entity.addPotionEffect(<potion:minecraft:glowing>.makePotionEffect(400,0)); //使用第一个ZenMethod添加药水效果
event.cancel(); //这行代码取消事件以消除玩家攻击值对实体生命产生的变动影响
//代码结束
}
else{
//第二段代码
player.sendChat("ZenSetter测试,对方生命值已被设置成:1点"); //可视化输出
entity.health = 1.0f; //将目标僵尸生命值修改成1点
player.sendChat("ZenMethod测试,目标实体发光效果已移除"); //可视化输出
entity.removePotionEffect(<potion:minecraft:glowing>); //使用第二个ZenMethod移除药水效果
event.cancel();//同上不解释
//代码结束
}
}
}
});
这里我们暂时不用管这个事件是怎样运行的(毕竟对于初学者而言这个事件的难度已经和简单的匠魂词条自定义持平了)。事件的逻辑是:当玩家攻击一个实体时,若对方没有发光(glowing)效果时则运行第一段代码:用ZenGetter返回实体受伤前生命值信息并用ZenMethod对实体添加一个20s的发光效果;若没有则运行第二段代码:用ZenSetter将实体的生命值设置成1并用ZenMethod消除实体的发光效果。实际运行效果如下:
实验体僵尸,初始生命:19点()
第一次攻击,由于没有发光效果,运行第一段代码
第二次攻击,由于有发光效果,运行第二段代码
事实上由于一、二段代码相互作用,这只僵尸无论玩家怎么砍它都死不了(
贴一张运行代码图
(实际上如果各位真正理解了ZenGetter/ZenSetter/ZenMethod,那上述事件各位能结合CrT的wiki读懂90%以上了()
5.尖括号调用
尖括号“<>”调用是魔改中的一个调用基础对象的方法。在对MC进行基础物品、药水效果、实体、流体、矿辞乃至伤害来源等对象进行调用时都会遇到,如:
//基础格式:
<type:modid:name>//全部小写
//例子:
<item:minecraft:apple>//表示苹果(这里item:可以省略)
<potion:minecraft:glowing>//表示发光药水效果
<entity:minecraft:zombie>//表示僵尸
//特殊例子:
<ore:ingotIron>//表示铁锭矿辞
<liquid:iron>//表示熔融铁液体(铁水)
可以理解为:<type:modid:name>可以表示某种“IType”下的(一个)基础对象,比如说<item:modid:itemname>是IItemStack类型的(一个)、由id为“modid”的模组添加的、名字是“itemname”的物品;比较特殊的是液体和矿辞,<ore:orename>和<liquid:liquidname>能直接表示一个IOreDict类的基础矿辞对象和一个ILiquidStack类的基础液体对象。这是ZenScript中非常重要的、调用基础对象的方法。
对于常用的调用对象:物品、矿辞、流体和药水效果,物品可以将需要查询的物品拿在手里,使用/ct hand指令查询其id、nbt和矿辞;流体可以使用/ct liquids将所有流体id导入crafttweaker.log文件中查看;药水效果可以使用/ct potions将所有药水效果的id导入crafttweaker.log文件。
6.ZenScript常用关键词及操作规范(基础)
实际上如果直接拿着ZenGetter/Setter/Method去写代码,那各位的代码很有可能会遇见各种各样的错误。这里JS会介绍部分基础的ZenScript的关键词,不仅能让你的代码避开各种错误,还能为你的代码增添不少色彩!
6.1 var和val
在上边我们已经了解到了var是一种声明变量的关键词,在ZenScript中还有一个声明变量的关键词叫val,与前者的区别在于val声明的变量在初始化后不能再度赋值修改(类似于C语言中的const关键词),比如说我把上边的代码修改一下:
//原代码:
var entity as IEntityLivingBase;
entity = event.entityLivingBase;
//修改后:
val entity as IEntityLivingBase;
entity = event.entityLivingBase;
报错:
正确的使用方法是将声明和初始化放在同一行:
val entity as IEntityLivingBase = event.entityLivingBase;
//当然var也能这样操作:
val entity as IEntityLivingBase = event.entityLivingBase;
6.2 as转换与instanceof判别转换
在上述的操作中估计各位会发现,变量的声明里使用了“as”这个关键词。而在操作过程中as除了声明变量类型还能强制转换数据类型(不过随意操作可能会造成难以估量的后果!)。笔者只建议在各种基础数据类型之间转换(如int、float、double之间相互转换,但可能会造成取整等数据丢失的问题)以及这些数据转换成string,使用方法:变量名+as+数据类型,如:
print((1.2f+1.5d) as int);//输出为2
关于ZenScript系统内部对基础数据类型的匹配请参考这里:click here.
实际上ZenScript对数据的类型匹配要求非常严格,笔者就有幸遇见一次需要float数据结果传入int数据导致一堆报错的问题。实际操作过程中也会经常碰见各种需要转换类型的情况,比如说用武器攻击实体时需要判断攻击者(一般只能获取为IEntityLivingBase)是否是玩家(IPlayer),这个时候就需要instanceof关键词了,一般会套用一个模板:
if(旧变量名 instanceof 目标类型){
var 新变量 as 目标类型 = 旧变量名;
//your code
}
//e.g. (导包等省略)
var entity as IEntityLivingBase;
//参数传入省略
if(entity instanceof IPlayer){
var player = entity;
//other code
}
具体机制则是:变量+instanceof+类型名 判断变量是否属于目标类型,并输出true(是)/false(否),用if承接判定结果,如果为真则用目标类型的新变量重新承接旧变量的信息。当然instanceof本身也可以当作一个判断类型的工具使用。不过笔者在这里还得要再三强调:匹配数据类型真的非常非常非常重要!!!错误匹配后果非常严重!!!请检查好诸如变量赋值、“==”判断前后、函数参数传入的类型匹配等问题!
(当你发现你的代码在对话框报出一行红色错误加一堆白色的、看不懂的数据(比特码)的时候估计很大可能性就是类型匹配出现问题了()
(开始算命()
6.3 isNull()与空指针
有时候ZenGetter会返回空指针,比如说我对一个受到虚空伤害的玩家索取伤害来源实体(虚空索敌),显然这里根本没有任何“实体”对ta造成伤害,使用相关ZenGetter会返回所谓“空指针”(即null),贸然使用会导致空指针报错。这个时候我们就需要使用isNull()来判定获取的数据是否为空(null),才能进行下一步操作,例子:
//导包等省略
var player as IPlayer;
//参数传入等省略
if(isNull(player.currentItem)){ //IPlayer类里有个currentItem的ZenGetter,可以获取主手的IItemStack信息,但由于玩家可能什么都没拿,贸然使用可能会报错空指针,所以得用isNull()判定一下
print("玩家主手无东西!"); //没东西则输出信息
}
else{ //有东西则运行代码
var mainhand as IItemStack = player.currentItem; //可以放心地用新变量存储数据了
//other code
}
//常用下列格式来避免null报错:
if(!isNull( ZenGetter )){
//your code
}
6.4“相等”判断
很多时候我们会有“比较两个对象是否相等”的需求。。对于int、float、double、long和string的基本类对象,我们可以直接使用“==”来判断。然而事实上,更常见的情况是我们需要判断其他类的两个对象是否“相等”,而很多时候仅仅只是需要对象“类型”相等。举个例子:我为了判断玩家主手上是否是“钻石剑”(只要是钻石剑就行了,不用管它耐久度多少、有什么附魔啥的),很多初学者会这样写:
//导包等省略
var player as IPlayer;
//假设这里我们已经获取了玩家的数据信息并且存入了player变量中:
var mainhand as IItemStack = player.currentItem; //运用上边提到的ZenGetter存入玩家主手物品数据信息
if(!isNul(mainhand)){ //排除空指针
if(item == <item:minecraft:diamond_sword>){ //用尖括号调用表示一个原版钻石剑的IItemStack对象,尝试判断是否是钻石剑(注意这里有错误)
//other code
}
}
然后这串代码就不出意外地出意外了,程序报错:Operator Not Support(操作器不支持)。
细细想来你会发现:IItemStack这个类是从很底层的类一层一层派生上来的,数据成员也有其他各种各样的类,包含了非常多的数据信息,直接使用“==”判断显然不够现实。这里提供一种通用的方法:获取string类型的物品id然后判断。
对于这个例子而言,我们仅仅只是需要“手上是钻石剑”,而IItemStack中有个ZenGetter(definition)可以获取物品的IItemDefinition类数据信息,对于后者我们也有一个Getter(id)可以获取物品的id信息,于是我们可以这样写:
//导包等省略
var player as IPlayer;
//假设这里我们已经获取了玩家的数据信息并且存入了player变量中:
var mainhand as IItemStack = player.currentItem; //运用上边提到的ZenGetter存入玩家主手物品数据信息
if(!isNul(mainhand)){ //排除空指针
if(item.definition.id == <item:minecraft:diamond>.definition.id){ //通过id判断玩家手中是否是钻石剑,注意Getter可以连续用“.”连接套用,
//other code
}
}
id判断法常用于药水、生物、液体甚至伤害类型等判断,但对于物品而言这个方法并非最优。IItemStack提供了一个ZenMethod(Boolean matches(IItemStack item))(准确来说有三个,但这个最常用)可以代替上述代码,于是判断部分可以改成:
if(<item:minecraft:diamond>.matches(mainhand)){ //matches方法判断玩家手中是否是钻石剑
//other code
}
6.5./ct syntax重载与错误改正
CrT提供了大量的以/ct开头的命令(官方wiki连接:Click Here),这里介绍一个魔改中重要且常用的的命令:/ct syntax。由于Forge本身底层代码的限制,CrT并不能对修改保存后的.zs代码文件进行重新加载并直接应用到游戏中(比如CrT修改的游戏配方、CoT自定义的各种物品等),只能够通过一次又一次的“关闭-打开”游戏对代码进行重新检测加载(“冷加载”),特别是在模组数量较多、部分模组加载时间过长(点名批评IAF、Treasure2)时,重新加载一次所需时间成本过于高昂。所以我们需要另一种高效方法来对我们的.zs脚本文件进行(初步)调试——这便是/ct syntax的作用所在。/ct syntax的作用便是在不关闭游戏窗口的前提下对scripts及其子文件夹中所有的.zs文件进行重新加载检测(“热检测”),并且如果代码有错误的话可以及时抛出报错,方便玩家进行实时调试修改。
不过,/ct syntax也有局限——它只能够“检测代码”而不是“加载代码”,并且面对尖括号错误引用报错、部分比特码报错(见下文)等报错它并不会抛出(请珍惜第一次报错的报告,如果真碰到那只能老老实实重开游戏重新加载......)。
这里我们以下列带有许多错误的代码为例进行讲解:(自定义合成配方并不在本教程的教学范围之内,此处仅作举例,详情请自行查阅wiki和相关教程;具体报错提醒请看友谊妈的报错文档:Click Here)
//错误类型:val赋值定义后试图修改,抛出报错:not a valid Ivalue
val a as int = 10;
a = 15;
var shapedRecipeName as string = "ironToIronwood";
//错误类型:1.未定义“dirtToDiamond”,或是未给该字符串打上半角双引号"",抛出报错:cannot find dirtToDiamond;
//2.丢失addShapeless(//参数)最后一个“)”,抛出报错:“)”expected
recipes.addShapeless(dirtToDiamond,<item:minecraft:diamond>,[
<item:minecraft:dirt>,<item:minecraft:dye:12>
];
//错误类型:1.将“<item:minecraft:iron_ingot>”打成了“<item:minecraft:iron>”,
//抛出报错:Trying to access Ghost item before it's ready:<item:minecraft:iron>;
//2.在addShaped(//参数)后边缺少“;”,抛出报错:“;”expected
recipes.addShaped(shapedRecipeName,<item:twilightforest:ironwood_ingot>,[
[<item:minecraft:dye:10>,<item:minecraft:dye:10>,<item:minecraft:dye:10>],
[<item:minecraft:dye:10>,<item:minecraft:iron>,<item:minecraft:dye:10>],
[<item:minecraft:dye:10>,<item:minecraft:dye:10>,<item:minecraft:dye:10>]
])
(实际如果装了拓展包那部分语法不规范错误都会有相应的提示)
游戏内实际报错,由于代码不规范报错优先级较高,游戏停止加载其他报错,故无论是初次加载还是/ct syntax都只有一个报错
此处修改了第8行的错误并保存后,再次使用/ct syntax重载:
第二个语法不规范报错:缺少“;”
修改+/ct syntax:
修改后再次/ct syntax:
此时错误“看似”都解决了,我们重新加载游戏,报错后顺手/ct syntax,可以发现/ct syntax并没有发现错误:
这便是/ct syntax的局限之处。在笔者改正错误并重启游戏后,成功加载了自定义配方:
(实际上CrT的事件部分等代码可以使用Zen Utils模组直接进行热重载,具体详见wiki:Click Here论友谊妈为什么是神,尽管这只是题外话和自定义匠魂特性部分(几乎)无关)
7.报错处理(补充)
所谓“代码一时爽,debug火葬场”,ZenScript报错问题是新手上手时一定会碰到的一大难关。如何识别报错、改正报错,这是新手走向熟练的过程中必备的技能。
实际上上文讲述的各种操作规范,其主要目的都是为了规避报错(如isNull判定规避空指针、instanceof判定规避类型匹配错误等)。许多常见的报错友谊妈写的文档里边都提及了,可以点击这里跳转查看:Click Here。此处补充介绍一种非常常见的报错的一种可能处理方法:比特码报错。
什么是比特码报错?就是在游戏中出现的一堆白色的、你根本没法看懂的报错信息。这里使用笔者写在隔壁的研究匠魂词条触发机制教程关于“测试特性”的自定义特性删掉部分代码为例:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
val aTrait = TraitBuilder.create("d_trait");
dTrait.color = 0xb06aff;
dTrait.localizedName = "测试特性D" ;
dTrait.localizedDescription = "这是个测试特性!\n结算攻击时输出基础伤害和截至触发到该条特性时的伤害" ;
dTrait.calcDamage = function(trait, tool, attacker, target, originalDamage, newDamage, isCritical) {
if(!attacker.world.remote){
attacker.sendChat("D特性触发!");
attacker.sendChat("初始伤害为:" ~ (originalDamage as string));
attacker.sendChat("截至触发到该条特性时伤害为:" ~ (newDamage as string));
}
return newDamage;
};
dTrait.register();
加载该代码,会出现类似于下图报错消息,这种便是“比特码报错”:
很多新手看到这个消息可能直接被吓懵了,因为这种报错消息不仅会直接占满整个对话框可显示的上限导致新手甚至无法看到整个报错的开头,其可怖的信息量少说能占满一页docx文档(多则无上限,十几页甚至几十页都有可能)。下图便是上图报错部分完整复制粘贴在docx文档中的截图:
为了解决这个报错,我们必须得有相应的处理思路和处理方法。首先必须说明的一点是:根据笔者经验,比特码报错一般与“类型操作时的匹配错误”相关,比如说未导包、“==”两边类型不同、A类调用非父类的B类接口、函数参数传递类型不匹配等,这是我们进行代码改正所需要明确的方向。
有了大概方向,笔者建议其次获取完整的相关错误报告,这里笔者建议打开.minecraft\logs路径下的latest.log(或者对应时间的压缩包)日志文件,并将所有以“[时:分:秒] [Client thread/INFO]: [CHAT] §cERROR: ”开头的内容复制粘贴在docx文档中(如上图)。(或者你也可以用/ct syntax重载代码,找出简短的、能够读懂、修改的报错信息并修改对应错误)
由于比特码部分并非属于凡人能够轻易读懂的范畴,我们不妨无视具体内容,转而看向那些以“[时:分:秒] [Client thread/INFO]: [CHAT] §cERROR: ”开头的报错信息:
[22:36:38] [Client thread/INFO]: [CHAT] §cERROR: Trait\TestTrait2.zs:12 > No such member in crafttweaker.entity.IEntityLivingBase: sendChat
[22:36:38] [Client thread/INFO]: [CHAT] §cERROR: Trait\TestTrait2.zs:13 > No such member in crafttweaker.entity.IEntityLivingBase: sendChat
[22:36:38] [Client thread/INFO]: [CHAT] §cERROR: Trait\TestTrait2.zs:14 > No such member in crafttweaker.entity.IEntityLivingBase: sendChat
[22:36:38] [Client thread/INFO]: [CHAT] §cERROR: [contenttweaker]: Error executing {[0:contenttweaker]: Trait\TestTrait2.zs}: Bad type on operand stack......
可以看到前三条已经有我们能够找出原因并且可以修改的错误:No such member in crafttweaker.entity.IEntityLivingBase: sendChat,这是典型的调用接口错误(友谊妈文档原文:“没有找到方法”)的报错。为了解决这个报错问题,我们必须得对源代码12至14行进行接口调用和类型匹配检查:
attacker.sendChat("D特性触发!");
attacker.sendChat("初始伤害为:" ~ (originalDamage as string));
attacker.sendChat("截至触发到该条特性时伤害为:" ~ (newDamage as string));
由于attacker在calcDamage函数(关于calcDamage函数详细信息可以查看下文实例介绍2.1部分)中属于IEntityLivingBase类,而sendChat(string textYouWannaSend)这个ZenMethod是IPlayer类的接口。IEntityLivingBase包含但并不完全等同于IPlayer类(诸如僵尸等生物不属于IPlayer类),所以在未经instanceof判断前直接使用IPlayer的ZenMethod接口会导致接口调用报错,从而导致比特码报错的出现。此处笔者使用instanceof判断并转换类型后重新加载脚本,接口调用报错便消失了(此处使用/ct syntax重载代码):
修改后代码如下:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
import crafttweaker.player.IPlayer;
val dTrait = TraitBuilder.create("d_trait");
dTrait.color = 0xb06aff;
dTrait.localizedName = "测试特性D" ;
dTrait.localizedDescription = "这是个测试特性!\n结算攻击时输出基础伤害和截至触发到该条特性时的伤害" ;
dTrait.calcDamage = function(trait, tool, attacker, target, originalDamage, newDamage, isCritical) {
if(!attacker.world.remote){
if(attacker instanceof IPlayer){
var player as IPlayer = attacker;
player.sendChat("D特性触发!");
player.sendChat("初始伤害为:" ~ (originalDamage as string));
player.sendChat("截至触发到该条特性时伤害为:" ~ (newDamage as string));
}
}
return newDamage;
};
dTrait.register();
重启游戏,比特码报错也随之消失:
然而,根据笔者经验,像上述示例的、通过简单修改三个简短报错便可以解决的比特码报错往往属于“撞大运”。该比特码报错其实是笔者为了“制造”而“制造”的报错,也因此笔者对它有着明确的错误类型认知和清晰的修改思路。实际操作中的比特码报错往往不会如同这个示例这样轻易修改便能解决就连群里的大佬都说“比特码报错,开始算命”。不过既然属于报错,那一定有解决的方法,笔者在此总结一下相关解决方法和步骤:
1.明确方向。比特码报错一般与类型操作时的不匹配有关,因此在检查相关代码时需要多多留心:是否存在类操作上的不规范、不匹配问题(如未导包、接口调用错误等);
2.从简单入手。比特码报错前往往会有一些简短易懂的报错,这些报错可以通过查看日志、或者用/ct syntax重载文档发现。优先参照友谊妈的文档检查相关代码,解决这类问题;
3.逐段注释,重复加载。在2.所述的问题解决但比特码报错尚未解决时,便需要对代码进行逐段注释分析排查,并多次重载游戏,直至发现错误代码段,重点排查相关类型的匹配。若碰到难题请优先尽力自行解决,最后再考虑向大佬求助。
(警告:向他人求助时强烈建议描述时附带代码整体需求、报错完整描述、完整源代码截图和相关日志段落完整报错信息,报错信息强烈建议粘贴至该网站:https://mclo.gs/进行保存分享!)
当然,最重要的还是得规范代码风格,积累处理代码报错的经验,尽力避免各类报错的发生。也祝愿赛博佛祖能够保佑屏幕前的各位,代码不出错,出错易修复!
二、自定义词条思路解析
接下来便是本篇教程的正题了。说实话JS也没想到前边要点写了这么多复制粘贴到doc文档一看已经7k+字了(不过只要把上边的要点吃透,接下来的事情就好说多了。
1.事件概论
警告:本篇教程所提及的事件构成思维方式和“触发条件”、“触发结果”等概念仅作为萌新入门的教学方法使用,仅适用于本篇教程!!!和其他人交流时请注意将整个事件描述清楚!!!必要时请附上源代码和完整需求!!!在未被他人理解的前提下擅自使用相关概念、思维交流所引发的各种后果本教程不负任何责任!!!
(笔者有幸在萌新时期遇到过因单纯只是问了怎么达成回复工具耐久这个触发结果而被群里各种大佬轮番怼甚至被质疑“有没有写过事件”的事情)(最后还是友谊妈出手救的场)(友谊妈yyds!!!)
(不过这个问题下边实例中笔者会详细解析,也算得上是一个萌新必踩的坑了哈哈哈)
什么是事件?JS在这里把它为“一件事情”。既然是事情,那肯定来龙去脉,也即发生的原因、发生的结果,这俩放在事件里边便称为事件的“触发条件”和“触发结果”。它们是事件中非常重要的组成部分,我们接下来的构建自定义词条的思维方式也将会围绕着这两点展开。
MC中的事件,形式各种各样:明显的,如tnt爆炸、点燃地狱门、击败末影龙;不明显的,如玩家攻击生物、放置方块,乃至玩家的各种各样交互及其结果甚至游戏内昼夜交替、日月扭转,这些都属于MC中的“事件”。以tnt爆炸为例,我们分析一下它的触发条件和结果:该事件的触发条件是:(1)有一个点燃的tnt实体;(2)tnt实体到达爆炸时间,而该事件的触发结果是:(1)检测范围内存在的实体(以及相隔的方块以计算减伤),并对实体造成伤害(破坏);(2)计算1k+条爆炸射线路径上的方块并试图抹除生成掉落物;(3)播放粒子效果动画和爆炸音效。可以看到,这个事件成功地被我们拆分成了两部分,每一部分都有详细的步骤。对于我们想要构建的匠魂词条效果,我们也可以用相同的方法进行分析。尽管这个步骤很繁琐,但只要成功上手后就可以将这个步骤内化于心,我们就可以自然地写出我们需要的词条效果(事件效果)了。
还记得之前提到的ZenGetter/Setter/Method和尖括号调用嘛?它们将是我们构建事件两大部分的重要帮手:ZenGetter用于获取对象信息,ZenSetter用于设置对象信息,ZenMethod用于特殊操作(如获取状态、比对信息、添加效果等),而尖括号调用则可以直接创建基础对象。下文所述实例将会对上文所讲述的(几乎所有)基础知识进行综合运用,请务必对上文所提及的知识点有所掌握。
在这里补充一点:MC中匠魂词条的事件触发条件。CoT为匠魂2的联动提供了部分(很常用的)事件函数接口,所有相关参数已经提前赋值,只需要按照相应模板操作编写代码即可。具体可点击这里查阅:官方wiki 汉化链接 汉化备用链接
2.实例分析
在我们开始前,有几点必须声明:尽管CrT功能强大,但也并非万能,很多功能都很难(甚至无法)实现(部分会涉及游戏更加底层的代码机制,而这部分已经不是简单一个CrT及其附属模组能够解决的了,转而需要modding,也即编写模组才可能解决)。只有在一次又一次的事件编写累积经验后,才能有思路有方法。所以请各位暂时放下自己的脑洞,从基础开始一点一点学习编写。相信不久之后,各位就能够自己编写事件、完成自己的脑洞了。
2.1.概率附加药水效果
各位或许见过以下特性:铅的剧毒、冰的霜冻、钢的锐利,这些都能在攻击时为敌方上debuff;而如暮色森林添加的坚毅则可以为自己上buff。附加药水效果的特性是自定义特性中最简单的一类,只需要调用现有的药水效果即可轻松达成目标。
这里我们先看看这个脑洞:命中时对敌方添加10s的漂浮效果。在这里,我们拆分一下这个事件:(在实现部分我会用“(【条件/结果】)”的形式指明代码作用)
· 事件的触发条件是:命中敌人
· 事件的触发结果是:添加10s的“漂浮”药水效果
我们将这个特性暂且命名为“漂浮”,先把创建特性的部分给写好:(具体解析请查阅其他教程,这里不是重点)
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
import crafttweaker.entity.IEntityLivingBase;
import crafttweaker.potions.IPotion;
var levitation = TraitBuilder.create("levitation");
levitation.color = 0x66CCFF;
levitation.localizedName = "漂浮";
levitation.localizedDescription = "咒兰之力!\n让对手漂浮10s!";
//code
levitation.register();
接下来是关键部分:我们想要这个特性在命中敌人的时候触发(【条件】),记得这里优先查阅匠魂2的事件函数接口。经过查阅,有以下有关攻击敌人的事件函数可供选择:
在对对方造成伤害前的onHit函数
在对对方造成伤害后的afterHit函数
在对对方造成伤害前、确定此次攻击是否暴击的calcCrit函数(实际上这个也能用)
攻击计算暴击伤害(?)时调用的calcDamage函数(实际上这个也还能用)
显然在这个实例中使用onHit和afterHit并没有区别(但在关乎伤害计算等的时候需要注意药水添加的时机,比如添加Potion Core模组的虚弱(Vulnerable)效果),而选后边两者会给写代码带来不必要的麻烦。这里我们选onHit函数即可:
levitation.onHit = function(thisTrait, tool, attacker, target, damage, isCritical) {
//Code
};
参数解析:
thisTrait表示目前调用本条事件函数的(Trait类的)特性(具体到实例则是:自定义特性“漂浮”,levitation);
tool表示IItemStack类的、带有这条特性的匠魂工具;
attacker表示IEntityLivingBase类的攻击者(不一定是玩家!);
target表示IEntityLivingBase类的受攻击者;
damage是float类型的伤害数值(包含暴击);
isCritical则是bool类型的、表示是否暴击的参数。
此处我们需要对敌方添加药水效果,事件函数已经将受攻击生物的数据信息放入IEntityLivingBase类的target局部变量中了。我们只需要直接拿来用即可。
前文提到:IEntityLivingBase有个ZenMethod:void addPotionEffect(IPotionEffect potionEffect)可以用来添加药水效果,这里可以直接套用模板添加效果,写在上文的//code区域:(【结果】)
//模板:注意持续时间Duration参数的单位是tick(1/20s,0.05s),药水强度Amplifier=游戏内药水效果等级-1,如力量II的Amplifier是1,速度I的Amplifier是0
entity.addPotionEffect(<potion:modid:potionid>.makePotionEffect( int Duration ,int Amplifier));
//实例:
levitation.onHit = function(thisTrait, tool, attacker, target, damage, isCritical) {
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
};
补上事件函数所需要的代码规范,整个特性代码如下:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
import crafttweaker.entity.IEntityLivingBase;
import crafttweaker.potions.IPotion;
var levitation = TraitBuilder.create("levitation");
levitation.color = 0x66CCFF;
levitation.localizedName = "漂浮";
levitation.localizedDescription = "咒兰之力!\n让对手漂浮10s!";
//核心代码
levitation.onHit = function(thisTrait, tool, attacker, target, damage, isCritical) {
if(target.world.remote) return;//服务端处理,代码规范
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
};
levitation.register();
将上述代码写进一个txt文档中并将后缀改成.zs扔进.minecraft的script文件夹中,并将id为“levitation”的特性用CoT添加到自定义材料中,或者使用ZenTraits模组通过ZenScript脚本/Tweakers Construct模组通过修改cfg文件添加到已有材料中测试效果,这里我选用Tweaker Construct模组作为演示,将特性添加到暮色森林的钢叶材料中:
(如果没有下边这一堆东西请将上边的B:"Fill Defaults Traits"=false 处改成true并重新加载游戏)
加载游戏:(我恨Treasure 2!和IAF模组,这俩严重拖累我游戏加载时间!!!)
可以看到书中成功出现我们的自定义特性“漂浮”,接下来测试效果:
打造的一把西洋剑,用海绵去掉攻击力(同调(重置)为我自定义的特性,下文会详解此特性)
僵尸:I'm free!
可以看到特性成功生效了!
特性升级!
这回我们加点料:每次攻击都会有50%的概率为敌方添加10s的漂浮和虚弱效果,并为我方添加10s的力量效果,虚弱和力量最多叠加至III,最长持续至60s,每次判定互相独立。
很显然,条件变多了,但我们也别怕,把整个事件拆分一下:
由于对于三个药水效果,我们都是要去“给予”或者“尝试叠加强度/时间”,不妨先解决第一条:漂浮效果的给予与叠加,对此我们有以下分析结果:
· 触发条件:(1)命中敌人;(2)随机数判定;(3)检测敌人的漂浮效果;
· 触发结果:(1)随机数判定失败,直接结束事件;(2)随机数判定成功,敌人没有漂浮效果,则添加10s的漂浮效果;(3)随机数判定成功,敌人有漂浮效果,若小于60s则尝试延长10s,最多不超过60s;(4)若大于60s则事件结束;
一步一步来,首先我们先把上边代码改造改造,构建主体先留着:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
import crafttweaker.entity.IEntityLivingBase;
import crafttweaker.potions.IPotion;
var levitationplus = TraitBuilder.create("levitationplus");
levitationplus.color = 0x66CCFF;
levitationplus.localizedName = "漂浮plus!";
levitationplus.localizedDescription = "咒兰恶魔的力量在你的血管里流动!\n每次攻击都会有50%的概率为敌方添加10s的漂浮和虚弱效果,并为我方添加10s的力量效果,虚弱和力量最多叠加至III,最长持续至60s,每次判定互相独立。";
//code
levitationplus.register();
onHit函数解决了命中问题(【条件1】),首先我们先解决药水效果添加的问题(【结果2、3】),写出初步的代码:
levitationplus.onHit = function(thisTrait, tool, attacker, target, damage, isCritical) {
if(attacker.world.remote) return;
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
};
这里我们需要一个随机数检测器用来检测是否触发【条件1】,从target里用名为world的ZenGetter获取IWorld信息并用变量world储存(注意导包!),可以套用模板world.random.nextDouble()从IWorld处获取0-1之间的一个double类型的随机数(wiki链接)并和设定概率比较,从而构成一个随机数判断器:
levitationplus.onHit = function(thisTrait, tool, attacker, target, damage, isCritical) {
var world as IWorld = target.world;//ZenGetter获取并存储IWorld信息,注意导包!
if(world.remote) return;//代码规范不说明
if(world.random.nextDouble()<0.5d){//随机数和设定概率之间的比较
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
}
};
【结果1】在随机数判断器if的这一步下已经被解决了,下一步我们来解决药水延长效果(【结果3】),这里需要用IEntityLivingBase类中的ZenMethod:IPotionEffect getActivePotionEffect(IPotion potion)获取特定的IPotionEffect类的药水效果,并且用ZenGetter获取该药水效果的时长(官方wiki),用一个变量lvdura(Levitation duration)存储:
var lvdura as int = target.getActivePotionEffect(<potion:minecraft:levitation>).duration;
如果时长小于60s(1200ticks)则延长10s(200ticks),否则延长至60s(【结果3】),可以使用min来简化,代码为:
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(
min(1200 , lvdura + 200)//min(a,b)返回较小值(注意!这里a和b都会被转化为int类型!返回值也是int!!!),这里单独把这一块放出来列成一行,实际不影响代码运行
,0));
实际上还有两点没有考虑到:如果对方身上带有大于60s的漂浮效果(【结果4】),和如果对方并没有携带漂浮效果(【结果2】)。
对于前一点:实际操作会发现,addPotionEffect这个ZenMethod会先比较新旧药水强度,再比较药水时间,最后考虑添加效果。如果新药水效果的强度大于原有药水强度,则会直接将新药水效果覆盖旧药水效果,无论时间长短;如果新药水强度等于旧药水强度,则保留事件最长的那个;如果新药水强度低于旧药水强度,则添加失败,ZenMethod直接结束。
对于后一点,则需要使用isNull()判断空指针,后添加药水效果(【结果2】):
if(isNull(target.getActivePotionEffect(<potion:minecraft:levitation>))){
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
}
整合一下代码,注意lvdura的定义需要在判定非空指针后使用:
if(isNull(target.getActivePotionEffect(<potion:minecraft:levitation>))){
//空指针,对方未携带药水效果,添加效果
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
}
else{
//对方有药水效果,尝试覆盖
var lvdura as int = target.getActivePotionEffect(<potion:minecraft:levitation>).duration;
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(min(1200 , lvdura + 200),0));
}
将其整合进原代码中,完成“漂浮”药水效果部分:
levitationplus.onHit = function(thisTrait, tool, attacker, target, damage, isCritical) {
var world as IWorld = target.world;//ZenGetter获取并存储IWorld信息,注意导包!
if(world.remote) return;//代码规范不说明
//漂浮部分
//随机数生成器
if(world.random.nextDouble()<0.5d){
//药水时长检测
if(isNull(target.getActivePotionEffect(<potion:minecraft:levitation>))){
//空指针,对方未携带药水效果,添加效果
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
}
else{
//对方有药水效果,尝试覆盖
var lvdura as int = target.getActivePotionEffect(<potion:minecraft:levitation>).duration;
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(min(1200 , lvdura + 200),0));
}
}
//其余部分
};
仿照上边的方法,使用IPotionEffect的一个ZenGetter获取药水强度并进行比较,并将“target”换成“attacker”以为自身添加药水效果,可以完成后边两个药水效果的事件代码:
levitationplus.onHit = function(thisTrait, tool, attacker, target, damage, isCritical) {
var world as IWorld = target.world;//ZenGetter获取并存储IWorld信息,注意导包!
if(world.remote) return;//代码规范不说明
//漂浮部分
//随机数生成器
if(world.random.nextDouble()<0.5d){
//药水时长检测
if(isNull(target.getActivePotionEffect(<potion:minecraft:levitation>))){
//空指针,对方未携带药水效果,添加效果
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(200 ,0));
}
else{
//对方有药水效果,尝试覆盖
var lvdura as int = target.getActivePotionEffect(<potion:minecraft:levitation>).duration;
target.addPotionEffect(<potion:minecraft:levitation>.makePotionEffect(min(1200 , lvdura + 200),0));
}
}
//虚弱部分
if(world.random.nextDouble()<0.5d){
//药水时长检测
if(isNull(target.getActivePotionEffect(<potion:minecraft:weakness>))){
//空指针,对方未携带药水效果,添加效果
target.addPotionEffect(<potion:minecraft:weakness>.makePotionEffect(200 ,0));
}
else{
//对方有药水效果,尝试叠加
var wkdura as int = target.getActivePotionEffect(<potion:minecraft:weakness>).duration;
var wkampl as int = target.getActivePotionEffect(<potion:minecraft:weakness>).amplifier;
//药水等级检测
if(wkampl<2){
//药水等级小于3,等级叠加
target.addPotionEffect(<potion:minecraft:weakness>.makePotionEffect(200 ,wkampl + 1));
}
else{
//药水等级大于等于3,时间延长
target.addPotionEffect(<potion:minecraft:weakness>.makePotionEffect(min(1200 , wkdura + 200),2));
}
}
}
//力量部分
if(world.random.nextDouble()<0.5d){
//药水时长检测
if(isNull(attacker.getActivePotionEffect(<potion:minecraft:strength>))){
//空指针,对方未携带药水效果,添加效果
attacker.addPotionEffect(<potion:minecraft:strength>.makePotionEffect(200 ,0));
}
else{
//我方有药水效果,尝试叠加
var stdura as int = attacker.getActivePotionEffect(<potion:minecraft:strength>).duration;
var stampl as int = attacker.getActivePotionEffect(<potion:minecraft:strength>).amplifier;
//药水等级检测
if(stampl<2){
//药水等级小于3,等级叠加
attacker.addPotionEffect(<potion:minecraft:strength>.makePotionEffect(200 ,stampl + 1));
}
else{
//药水等级大于等于3,时间延长
attacker.addPotionEffect(<potion:minecraft:strength>.makePotionEffect(min(1200 , stdura + 200),2));
}
}
}
};
最后把这串代码塞入构造主体中,如下图:(别忘记导包IWorld!!!!!!)(否则你会面对四页的比特码报错)(别问我怎么知道的()
加载游戏:(CR方框是我为了排版方便输入的换行符,后期删除换行即可消除)
匠魂宝典显示
一把新锻造的西洋剑
测试僵尸
攻击多下后效果(然而后边力量III先叠上去导致僵尸被杀死了()海绵减伤被bypass了()
僵尸死了,换个铁大哥来测试效果
力量III的叠加
代码成功生效了!你成功向简单的自定义特性迈出了一大步!
2.2.增伤与治疗(旧)
在上一节我们成功实现了添加药水效果和概率触发的自定义匠魂特性,但或许一个单纯的添加药水规则还满足不了你偌大的脑洞,你希冀着能一刀999秒掉怪物,或者是你想复刻一手匠魂原版的吸血效果,战斗中“没有一滴鲜血是自己的”。这一节JS将会介绍如何达成增伤、减伤和治疗的简单方法。
先来看第一个脑洞:(伤害增加)
· 特性名:精锐杀手
· 特性效果:对血量高于100的敌人以及所有Boss造成的伤害提高100%,并附加2%的最大生命值伤害。
老规矩,先上特性注册代码:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
var eliteKiller = TraitBuilder.create("elite_killer");
eliteKiller.color = 0x142857;
eliteKiller.localizedName = "精锐杀手";
eliteKiller.localizedDescription = "老子打的就是精锐!\n对血量高于100的敌人以及所有Boss造成的伤害提高100%,并附加2%的最大生命值伤害。";
//code
eliteKiller.register();
然后拆分这个事件:
· 触发条件:(0)攻击敌人时;(1)检测敌方是否是boss;(2)检测敌方(最高)血量是否高于100
· 触发效果:(1)造成伤害提高20%;(2)附加2%的最大生命值伤害
开始写效果:第一步,解决事件函数的调用问题(【条件0】),这里选用上文提到的calcDamage事件函数,先抄模板:(注意calcDamage需要一个返回值)
(这里补充说明关于calcDamage函数的一点:尽管函数说明是“计算暴击伤害”,实测没有暴击(如跳劈)的时候也能成功增伤,所以这里直接套用即可)
(然而JS又翻了翻官方的wiki并仔细读了读,关于这一点几乎能够确定是友谊妈的中文教程出错了)
(官方原文:Called when an entity is hit, but still before the damage is dealt and before the crit damage is added.The crit damage will be calculated off the result of this. 并没有说明是只能修改暴击伤害)
eliteKiller.calcDamage= function(thisTrait, tool, attacker, target, originalDamage, currentDamage, isCritical) {
//code
return currentDamage;//或修改后的值
};
第二步,检测敌方是否是boss(【条件1】)。关于这一点,我们得翻一翻官方wiki中关于IEntityLivingBase的类接口(这里是链接)。
遗憾的是,我们并没有在IEntityLivingBase的接口里边找到任何和判断生物是否是Boss相关的ZenGetter/Setter/Method,但是我们注意到这一句话:
翻译:IEntityLivingBase类由IEntity类派生而来,这意味着IEntity类的任何(接口)函数,IEntityLivingBase都能调用。(略微意译了一下)
我们不妨去IEntity类接口碰碰运气,看看是否有相关的接口可以调用。果然,有这样一个接口可以判断:(ZenGetter表格,从左至右分别是GetterName、GetterMethod和Return Type (can be null))
于是我们可以直接用target.isBoss这个ZenGetter来判断目标是否是Boss。
判断生命值就比较简单了,直接调用IEntityLivingBase的ZenGetter:maxHealth,来获取对方的最大生命值(【条件2】)。
至于增伤部分,我们先定义一个变量存储原来的伤害,将其乘以2后加上目标2%的生命值便是修改后的伤害值。calcDamage函数需要返回一个float值用以确定伤害值,我们可以最后直接把自定义变量返回就行了(【结果1】)。(建议注意int类型和float类型的匹配问题,尽可能全部转为float类型来操作)
于是我们就有了如下代码:
eliteKiller.calcDamage= function(thisTrait, tool, attacker, target, originalDamage, currentDamage, isCritical) {
var damage as float = currentDamage;//用一个float类型的变量damage存储修改后的伤害值
if(target.world.remote)return damage;//代码规范问题,这个函数需要一个返回值,直接返回没有任何修改的damage就行了
if(target.isBoss){
damage = damage * 2.0f + target.maxHealth * 0.02f//伤害修改
}
else if(target.maxHealth>100){ //else if避免boss血量大于100时再次叠加伤害
damage = damage * 2.0f + target.maxHealth * 0.02f//伤害修改
}
return damage;
};
这里我们可以用“或”运算(||)简化代码:
eliteKiller.calcDamage= function(thisTrait, tool, attacker, target, originalDamage, currentDamage, isCritical) {
var damage as float = currentDamage;//用一个float类型的变量damage存储修改后的伤害值
if(target.world.remote)return damage;//代码规范问题不解释
if( target.isBoss || target.maxHealth>100 ){
damage = damage * 2.0f + target.maxHealth * 0.02f//伤害修改
}
return damage;
};
整合代码:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
var eliteKiller = TraitBuilder.create("elite_killer");
eliteKiller.color = 0x142857;
eliteKiller.localizedName = "精锐杀手";
eliteKiller.localizedDescription = "老子打的就是精锐!\n对血量高于100的敌人以及所有Boss造成的伤害提高100%,并附加2%的最大生命值伤害。";
//核心部分
eliteKiller.calcDamage= function(thisTrait, tool, attacker, target, originalDamage, currentDamage, isCritical) {
var damage as float = currentDamage;//用一个float类型的变量damage存储修改后的伤害值
if(target.world.remote)return damage;//代码规范问题不解释
if( target.isBoss || target.maxHealth>100 ){
damage = damage * 2.0f + targetmaxHealth * 0.02f;//伤害修改
}
return damage;
};
eliteKiller.register();
打开游戏测试一下效果:
这次换了把纸的西洋剑
用普通僵尸测试一下伤害,一次3点()
改用被我修改过血量后的、极光幽境里的皎月女王(Boss)测试,掉了14点()
(基础3点*倍率×2+最大生命值600*2%=18点,护甲减伤掉4点,属于合理的伤害范畴)
改用1k血的僵尸测试,掉了25点( × 12.5)
(基础3点*倍率×2+最大生命值1000*2%=26点,护甲减伤掉1点,属于合理的伤害范畴)
特性自定义成功!
这里必须要对上文查阅到“isBoss”接口的过程多说几句:对于那些你“和xx类相关、认为该类很大可能包含的、有很大可能可以从该类中获取到”的数据,但又从该类的wiki中查询不到任何相关的接口,这个时候可以考虑从其父类(entends后边的那个类)(或者是父类的父类等)入手尝试,或者是用ZenGetter获取到其他类信息从而调用其(乃至其父类)接口。实际上这些接口信息的获取位置很大程度上依赖玩家对各个类的理解和编写zs脚本的经验,这里建议萌新先尽量尝试自己查找解决,如确认自己尽力都无法解决再去询问有经验的大佬(建议直接询问整个问题的解决方案)(必要时请附带完整需求和源代码!!)。
2.2 增伤与治疗(新)【23.9.9更新部分】
事实上笔者在撰写教程时一直在想,要怎么教、怎么把“事件的实现”给解决好这个问题。尽管事件可以被简单粗暴地拆解为“触发条件”和“触发结果”两部分,但实际操作起来各位可以发现,若是简单特性还好说,对于复杂特性,这一种方法显然就不够用了,而且事实上也不符合编写代码的习惯。接下来笔者将会用新的方式讲解事件的编写过程,这也是我分2.2旧和新的目的,希望各位能有所收获......吧hhh......
(P.S. 实际上是笔者在写2.2节的时候想起了崩铁火主的e开盾免伤和强化普攻回血的机制于是打算在MC里复刻一手结果代码写好后出bug把笔者整自闭了导致几乎一周都没更新......)(现在仍旧有bug,摆了hhh)
来看这个脑洞:(尝试复刻星铁火主技能机制的青春版)
· 特性名称:「炽然不灭的琥珀」
· 特性效果:玩家攻击时有概率对周围5*5*5的非己方实体额外造成2.0点固定伤害并点燃所有实体,该效果触发时造成伤害提高50%,并治疗玩家2.0点生命值。
由于对于复杂事件,触发条件和结果是一环套一环的,因此在简单捋清晰了触发的条件和结果后,我们需要一个思维导图,方便我们将这一连串的特性效果转化成代码。首先还是触发的条件和结果:
· 触发条件:(1)玩家攻击实体时;(2)随机数检测;(3)周围实体检测;
· 触发结果:(1)造成固定伤害;(2)点燃实体;(3)伤害提高;(4)治疗玩家
接下来我们便需要一个思维导图,由于整个代码的逻辑是线性的,笔者在这里可以直接笔述:(才不是懒得开思维导图软件排版呢)
[1]由于这个特性关乎到“攻击时”和“本次伤害增加”,这里需要调用上述的calcDamage函数以计算伤害,并在最后返回伤害值以达到提高伤害的目的;(【条件1】)
[2]calcDamage函数是在实体主手持有武器并攻击时生效,特性限制需要玩家攻击才能触发,所以需要检测攻击者是否是玩家;(【条件1】)
[3]玩家检测通过后,需要通过随机数检测,以确定是否触发特效;(【条件2】)
[4]随机数检测成功,需要处理以下三件事:1)检测周围实体并造成固定伤害;2)点燃周围实体;3)提高本次伤害;4)治疗玩家;(【条件3】【所有结果】)
[4.1]要检测周围实体,首先需要获取目标实体位置;(【条件3】)
[4.2]有了目标实体位置,我们需要用特定的ZenGetter/ZenMethod检测周围实体,并判定该实体非攻击者;(【条件3】)
[4.3]对周围实体造成2.0的伤害(生命值降低2.0)并点燃;
[4.4]治疗玩家;
[5]返回调整后的伤害值,结束特性。
至此我们已经有了大致的代码编写思路了,接下来便是“翻译”环节:
首先是注册新特性的前置代码,顺手把calcDamage函数的模板写进去,完成[1]:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
import crafttweaker.player.IPlayer;
import crafttweaker.entity.IEntityLivingBase;
var everBurningAmber = TraitBuilder.create("ever_burning_amber");
everBurningAmber.color = 0xcb4b16;
everBurningAmber.localizedName = "「炽燃不灭的琥珀」";
everBurningAmber.localizedDescription = "「烈定!」\n玩家攻击时有概率对周围5*5*5的非己方实体额外造成2.0点固定伤害并点燃所有实体,该效果触发时造成伤害提高50%,并治疗玩家2.0点生命值。";
everBurningAmber.calcDamage = function(trait, tool, attacker, target, originalDamage, newDamage, isCritical) {
//code
};
everBurningAmber.register();
然后是[2],由于attacker属于IEntityLivingBase类,因此我们需要一个instanceof判定是否是玩家;由于最后涉及到返回伤害值参数,这里我们定义一个叫dmg的变量储存目前的伤害值(默认值为currentDamage),同时注意代码规范问题:(前后省略)
everBurningAmber.calcDamage = function(trait, tool, attacker, target, originalDamage, currentDamage, isCritical) {
var dmg as float = currentDamage;//定义一个dmg的变量储存目前的伤害值
if(attacker.world.remote)return dmg;//事件代码规范不解释
if(attacker instanceof IPlayer){//instanceof判定转化
var player as IPlayer = attacker;//用一个叫player的变量储存玩家信息
//code
}
return dmg;//记得返回修改后的伤害值
};
对于[3],接入一个随机数检测器:
everBurningAmber.calcDamage = function(trait, tool, attacker, target, originalDamage, currentDamage, isCritical) {
var dmg as float = currentDamage;//定义一个dmg的变量储存目前的伤害值
if(attacker.world.remote)return dmg;//事件代码规范不解释
if(attacker instanceof IPlayer){//instanceof判定转化,注意这里使用了IPlayer类的关键词,需要导包!
var player as IPlayer = attacker;//用一个叫player的变量储存玩家信息
if(player.world.random.nextDouble()<0.1f){//随机数检测器,设定概率为10%
//code
}
}
return dmg;//记得返回修改后的伤害值
};
然后是[4.1]和[4.2],这里我们需要一个特殊的实体检测方法,这个方法是CrT源代码有的但并没有写进wiki里(感谢群里的大佬们!!!):
world.getEntitiesInArea( Position3f areaStart, Position3f areaEnd );//return list IEntity[]
输入长方体两个对角顶点坐标(Positon3f类),从目标维度获取该范围内的所有实体表单(IEntity[]),需要注意的是这里获取的是一个IEntity类的数组而并非一个具体的对象。对于数组我们不能直接操作,但我们可以选择使用遍历的方式“一个一个地”操作其中的对象。
让我们来一个一个解决问题,首先是目标区域长方体的“Position3f类”的(坐标)参数,对此我们先获取目标实体的位置信息([4.1]),查阅wiki很容易获得以下三个ZenGetter/Method(链接,注意要去IEntity类的界面找),但是注意!虽然wiki里写了有这个ZenGetter/Method,但实际直接写会给报错数组越界:(别问我是怎么知道的)
var Xt as double = target.getX();//或者是target.x,下同
var Yt as double = target.getY();
var Zt as double = target.getZ();
//报错
[contenttweaker]: Error executing {[0:contenttweaker]: everburningamber.zs}: 0, caused by java.lang.ArrayIndexOutOfBoundsException: 0
这里就需要把as double放在后边转换数据类型,这样才不会报错:
var Xt = target.getX() as double;//或者是target.position.x as double等,下同
var Yt = target.getY() as double;
var Zt = target.getZ() as double;
然后是长方体两个对角顶点的坐标创建,将上述坐标同时加减2便是起点/终点的坐标,这里使用Position3f提供的构造函数来创建:([4.2])
//构造函数:
crafttweaker.util.Position3f.create(float x, float y, float z);
//起点,注意这里并没有使用关键词as+类名称,不声明这点的前提是你很清楚你所定义的对象的类型到底是什么!
var areaStart = crafttweaker.util.Position3f.create( ( Xt + 2.0f ) , ( Yt + 2.0f ) , ( Zt + 2.0f ) );
//终点
var areaEnd = crafttweaker.util.Position3f.create( ( Xt - 2.0f ) , ( Yt - 2.0f ) , ( Zt - 2.0f ) );
再然后先套用getEntitiesInArea这个ZenMethod:
//获取目标实体的坐标
var Xt = target.getX() as double;
var Yt = target.getY() as double;
var Zt = target.getZ() as double;
//创建两个Position3f对象
//起点
var areaStart = crafttweaker.util.Position3f.create( ( Xt + 2.0d ) , ( Yt + 2.0d ) , ( Zt + 2.0d ) );
//终点
var areaEnd = crafttweaker.util.Position3f.create( ( Xt - 2.0d ) , ( Yt - 2.0d ) , ( Zt - 2.0d ) );
//使用ZenMethod(先套模板写出来再考虑操作的事情)
target.world.getEntitiesInArea( areaStart , areaEnd );
下一步是遍历操作,照样套用遍历的模板,注意这里自定义的变量entity表示获取的IEntity表单中的每个IEntity类实体的数据信息:
for entity in target.world.getEntitiesInArea( areaStart , areaEnd ){
//code
}
到此为止我们可以进行实体的类型判定,成功地解决了“获取范围内实体信息”的问题,下一个实体类型判定使用instanceof关键词即可:
for entity in target.world.getEntitiesInArea( areaStart , areaEnd ){
if(entity instanceof IEntityLivingBase){//记得导包!!!
var entitylivingbase as IEntityLivingBase = entity;
//code
}
}
接下来我们需要判定范围内实体非攻击者,这里需要进行IPlayer类判定并比较uuid:
for entity in target.world.getEntitiesInArea( areaStart , areaEnd ){
if(entity instanceof IEntityLivingBase){//记得导包!!!
var entitylivingbase as IEntityLivingBase = entity;
//IPlayer类判定
if(entitylivingbase instanceof IPlayer){//记得导包!!!
var areaplayer as IPlayer = entitylivingbase;
if(areaplayer.uuid!=player.uuid){//玩家uuid判定,非攻击者执行
//code
}
}
else{//非IPlayer类执行
//code
}
}
}
至此,[4.1]和[4.2]我们已经完成了,下一个便是[4.3]:造成固定伤害并点燃实体。
使用IEntityLivingBase的health ZenSetter和IEntity的setFire(int seconds) ZenSetter便可达成目的:
for entity in target.world.getEntitiesInArea( areaStart , areaEnd ){
if(entity instanceof IEntityLivingBase){//记得导包!!!
var entitylivingbase as IEntityLivingBase = entity;
//IPlayer类判定
if(entitylivingbase instanceof IPlayer){//记得导包!!!
var areaplayer as IPlayer = entitylivingbase;
if(areaplayer.uuid!=player.uuid){//玩家uuid判定,非攻击者执行
areaplayer.health -= 2.0f;//固定伤害2.0点
areaplayer.setFire(5);//着火5s
}
}
else{//非IPlayer类执行
entitylivingbase.health -= 2.0f;//固定伤害2.0点
entitylivingbase.setFire(5);//着火5s
}
}
}
[4.4]的治疗攻击者代码就比较简单了,直接使用player.health+=2.0f即可:
player.health+=2.0f;
更改一下dmg的值便可以达成[5]了:
dmg *=1.5f;
整理一下代码即可:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
import crafttweaker.player.IPlayer;
import crafttweaker.entity.IEntityLivingBase;
var everBurningAmber = TraitBuilder.create("ever_burning_amber");
everBurningAmber.color = 0xcb4b16;
everBurningAmber.localizedName = "「炽燃不灭的琥珀」";
everBurningAmber.localizedDescription = "「烈定!」\n玩家攻击时有概率对周围5*5*5的非己方实体额外造成2.0点固定伤害并点燃所有实体,该效果触发时造成伤害提高50%,并治疗玩家2.0点生命值。";
everBurningAmber.calcDamage = function(trait, tool, attacker, target, originalDamage, currentDamage, isCritical) {
var dmg as float = currentDamage;//定义一个dmg的变量储存目前的伤害值
if(attacker.world.remote)return dmg;//事件代码规范不解释
if(attacker instanceof IPlayer){//instanceof判定转化,注意这里使用了IPlayer类的关键词,需要导包!
var player as IPlayer = attacker;//用一个叫player的变量储存玩家信息
if(player.world.random.nextDouble()<0.1f){//随机数检测器,设定概率为10%
//获取目标长方体范围
var Xt = target.getX() as double;
var Yt = target.getY() as double;
var Zt = target.getZ() as double;
var areaStart = crafttweaker.util.Position3f.create( ( Xt + 2.0d ) , ( Yt + 2.0d ) , ( Zt + 2.0d ) );
var areaEnd = crafttweaker.util.Position3f.create( ( Xt - 2.0d ) , ( Yt - 2.0d ) , ( Zt - 2.0d ) );
for entity in target.world.getEntitiesInArea( areaStart , areaEnd ){
if(entity instanceof IEntityLivingBase){//记得导包!!!
var entitylivingbase as IEntityLivingBase = entity;
//IPlayer类判定
if(entitylivingbase instanceof IPlayer){//记得导包!!!
var areaplayer as IPlayer = entitylivingbase;
if(areaplayer.uuid!=player.uuid){//玩家uuid判定,非攻击者执行
areaplayer.health -= 2.0f;//固定伤害2.0点
areaplayer.setFire(5);//着火5s
}
}
else{//非IPlayer类执行
entitylivingbase.health -= 2.0f;//固定伤害2.0点
entitylivingbase.setFire(5);//着火5s
}
}
}
player.health+=2.0f;
dmg *=1.5f;
}
}
return dmg;//记得返回修改后的伤害值
};
everBurningAmber.register();
扔进游戏测试即可:(注意:为了测试效果明显测试时笔者已经调高了概率至50%、治疗量至10点、固定伤害量至10点)
实验体僵尸们
测试效果,第二刀才触发特性:玩家生命回复,僵尸受到范围性伤害并被点燃(该粒子效果来自于Quark模组)
特性自定义成功!
2.3.装备自我修复
装备自我修复!这是几乎每个MC懒人玩家都非常中意的特性。毕竟作为懒人,谁也不想自己辛辛苦苦打造的装备没用多久就因为损坏被扔进岩浆,或者还得重新放回装备组装台上花材料修复耐久。这一节我们来仿照“生态”、“同调”等能自回耐久的特性,重新设计一个新的装备自我修复的特性。脑洞由极光幽境模组(TA)的“修复宝珠”,我们来看看具体效果:
· 特性名称:月光凝晶
· 特性描述:根据月相在夜晚修复装备
· 特性具体效果:根据月相在夜晚修复装备,月越圆则修复概率越高、速率越快,在满月时达到最大,在新月时不修复。
我们来缕一缕特性的触发条件和效果:
· 触发条件:(1)是否是夜晚;(2)月相如何;(3)随机数检测;
· 触发结果:修复工具;
然后是整个特性的思路:
[1]把这个特性类比“生态”、“同调”,我们可以发现,修复检测几乎是每时每刻都需要进行的也就是说“触发条件还缺少‘每时刻检测’”。有了这一点,我们可以调用onUpdate函数,在游戏每个游戏刻启用检测:(【条件0】)
[2]有了每时每刻的检测触发,下一步便是检测是否是夜晚,才能决定是否修复;(【条件1】)
[3]如果是夜晚,那我们还得获取当日游戏日的月相;(【条件2】)
[4]根据月相我们可以设定修复的概率和耐久值,这里我们需要一个随机数检测器检测是否修复;(【条件3】)
[5]如果检测通过了,我们便可以编写相关代码修复装备。
有了思路,我们来写一下特性创建的前置代码并套用onUpdate函数模板([1]):
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
val crystalizing = TraitBuilder.create("crystalizing");
crystalizing.color = 0xf79eff;
crystalizing.localizedName = "月光凝晶" ;
crystalizing.localizedDescription = "月光之精华凝结于此\n根据月相在夜晚修复装备" ;
crystalizing.onUpdate = function(thisTrait, tool, world, entity, itemSlot, isSelected) {
//code
};
crystalizing.register();
[1]我们已经解决了,接下来我们来看[2],在开始之前别忘了代码规范:检测world.remote;游戏日的夜晚关乎游戏的时间(“什么时候是夜晚”)、世界维度(某些维度被判定为永夜,如暮色森林),这里可以考虑去IWorld类里寻找相关ZenGetter/Method,这里笔者已经给大家找好了一个ZenGetter:isDayTime()(返回Bool类型,检测是否是白天),把它取否就是检测是否是夜晚:
crystalizing.onUpdate = function(thisTrait, tool, world, entity, itemSlot, isSelected) {
if(entity.world.remote)return;//代码规范
if(!entity.world.isDayTime()){//判断是否“不是”白天
//code
}
};
对于[3],IWorld有个ZenGetter:getMoonPhase(),返回从0开始到7的int类型数据,值得注意的是,MC里0表示满月,7表示新月,所以对于月亮圆满度我们可以考虑这样写:
var nowMoonPhase as int = 7 - entity.world.getMoonPhase();
这样,nowMoonPhase的0表示新月,7表示满月,我们成功把月亮的圆满度正向量化了。
至于随机数......不懂的请参考前文案例,这里直接略过讲解过程;([4])
到这里我们需要考虑一下随机数检测的概率问题。笔者的想法是以上/下弦月(nowMoonPhase=1)为基准,期望每秒修复0.2点耐久,每多1点月亮圆满度就多0.2点耐久。游戏里边1tick=0.05s,因此基准则是每tick有1%的概率修复1点耐久。我们可以考虑把相关代码写进概率代码里,或者写进修复代码里,这里笔者选择了前者,顺手解决了检测问题,即新月随机数检测通过概率为0%,由此我们可以写下如下代码:([4])
if(entity.world.random.nextDouble()>=(0.01d*nowMoonPhase)as double){
//code
}
最后是[5],要修复的工具tool属于IItemStack类型,因此我们可以以“damage”为关键词去寻找相关数据接口。这里有一个必须要注意的点:如果说你想对工具本体的数据信息直接进行修改而不是生成一个新的工具,那请务必使用mutable():
然后翻到IMutableItemStack类的界面去找相关接口:
这里我们可以不用管数据的返回类型(等同于这个ZenMethod在尝试做了“损坏”这个动作后再依据结果返回一个bool值),务必注意的是这里的damage的意思不是“n.耐久”而是“v.损坏”,我们可以通过提供一个负值参数来达到修复的效果,于是我们有了如下的代码:
tool.mutable().attemptDamageItem(-1);
整合事件代码:
crystalizing.onUpdate = function(thisTrait, tool, world, entity, itemSlot, isSelected) {
if(entity.world.remote)return;
var nowMoonPhase as int = 7 - entity.world.getMoonPhase();
if(!entity.world.isDayTime()){
if(entity.world.random.nextDouble()>=(0.01d*nowMoonPhase)as double){
tool.mutable().attemptDamageItem(-1);
}
}
};
至此,代码完工了:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
val crystalizing = TraitBuilder.create("crystalizing");
crystalizing.color = 0xf79eff;
crystalizing.localizedName = "月光凝晶" ;
crystalizing.localizedDescription = "月光之精华凝结于此\n根据月相在夜晚修复装备" ;
crystalizing.onUpdate = function(thisTrait, tool, world, entity, itemSlot, isSelected) {
if(entity.world.remote)return;
var nowMoonPhase as int = 7 - entity.world.getMoonPhase();
if(!entity.world.isDayTime()){
if(entity.world.random.nextDouble()>=(0.01d*nowMoonPhase)as double){
tool.mutable().attemptDamageItem(-1);
}
}
};
crystalizing.register();
值得一提的是,这个特性是笔者自己创作的最初的第三个还是第四个特性,笔者当初的代码思路和此处展示的略有不同(还有修复数据效果也有不同),放在下文以供各位参考:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
val thistrait = TraitBuilder.create("crystalizing");
thistrait.color = 0xf79eff;
thistrait.localizedName = "月光凝晶" ;
thistrait.localizedDescription = "月光之精华凝结于此\n根据月相在夜晚修复装备" ;
thistrait.onUpdate = function(thisTrait, tool, world, entity, itemSlot, isSelected) {
var nowMoonPhase as int = 7 - entity.world.getMoonPhase();
if (!entity.world.isDayTime()){
if(tool.isDamaged){//isDamaged检测是否损坏,以针对损坏的装备进行修补
var random as double = entity.world.random.nextDouble();
if(random<=0.05){
tool.mutable().attemptDamageItem(-min(nowMoonPhase,(tool.maxDamage-tool.damage)));
}
}
}
};
thistrait.register();
以下是测试效果:
特性测试成功!
三、部分自定义案例解析
在这一节,笔者将会放出部分自己编写的(较为复杂的)自定义特性并附上相应的解析说明。该部分难度有明显提高,但同时代码也能够达成更多更好的效果。希望大家能够学有所获,也希望能有大佬指出其中的不足。
3.1. 同调(重置)
这个特性制作的灵感来源于作者希望削弱暮色森林中“钢叶”材料所提供的“同调”属性,毕竟原版的同调40个钢叶修复期望1点/s有点过于超模。因此笔者重新拟定了修复期望随物品栏中钢叶数量变化的函数,如下图:
图中x轴代表物品栏(包括副手)的钢叶数量,y轴代表每秒修复物品耐久的期望(平均每秒修复多少点耐久)。最上边紫色的线是第三个函数的渐近线,代表着第三个函数无论x多大,y的值都无法超过甚至达到这个值。两个折点坐标分别是(63,0)和(663,75)。在钢叶数量在63-663区间内,每一片钢叶增加每秒修复期望0.025点(1/40点)。
下边是特性对应的代码:
#loader contenttweaker
#modloaded tconstruct
import mods.contenttweaker.tconstruct.TraitBuilder;
import crafttweaker.player.IPlayer;
import crafttweaker.item.IItemStack;
var thistrait = TraitBuilder.create("synergy_remake");
thistrait.color = 0x52873a;
thistrait.localizedName = "同调(重置)";
thistrait.localizedDescription = "看!它所需要的钢叶变多了!\n工具栏里有至少64个钢叶时装备会开始自我修复,钢叶越多修复速率越快";
thistrait.onUpdate = function(thisTrait, tool, world, entity, itemSlot, isSelected) {
if(!entity.world.remote){
//cout表示玩家工具栏和副手的钢叶统计数量
var count as int = 0 ;
//expectation表示每tick(0.02s)工具修复期望
var expectation as double = 0.0d;
//check表示检测工具是否位于玩家物品栏和副手
var check as bool = false;
if(!isNull(entity)){//检测entity是否非空
if(entity instanceof IPlayer){//检测是否是玩家
var player as IPlayer = entity ;//player储存玩家信息
var item as IItemStack;//item用于储存检测中的物品信息
var fixAmount as int = 0;//用于储存该tick内修复值计数
//遍历操作,首先检测玩家工具栏的物品情况
for index in 0 to 8{//0-8分别是玩家工具栏从左至右的9个物品格子编号
item = player.getInventoryStack(index);//将玩家背包内第(index+1)个IItemStack传递给item
if(!isNull(item)){//该位置物品非空
if(tool.matchesExact(item)){//检测是否是所持工具,使用matchesExact表示精确匹配
check = true;
}
if (<item:twilightforest:steeleaf_ingot>.matches(item)){//检测是否是钢叶,若是,则count计数增加钢叶数量
count+=item.amount;
}
else if(<item:twilightforest:block_storage:2>.matches(item)){//检测是否是钢叶块,若是,则count计数增加钢叶块数量*9
count+=item.amount*9;
}
}
}
//同上,检测玩家副手(编号40)的物品情况
item = player.getInventoryStack(40);
if(!isNull(item)){
if(tool.matchesExact(item)){
check = true;
}
if (<item:twilightforest:steeleaf_ingot>.matches(item)){
count+=item.amount;
}
else if(<item:twilightforest:block_storage:2>.matches(item)){
count+=item.amount*9;
}
}
//期望计算(默认期望为0)
if (check&&count>63){//check通过表示工具在工具栏内,count>63限制开始修复时钢叶数量
if(count<=663){//若在64-663之间,则期望为线性增长
expectation = 0.025d * ((count - 63) as double) * 0.05d * 2.0d;
}
else {//若大于663,则期望为非线性增长
expectation = 2.0d * ( 1.0d - 1.0d / ( 1.0d + 0.1d * ((count - 63) as double) * 0.05d ));
}
}
//期望处理,由于该函数每0.05s调用一次,tool.mutable().attemptDamageItem(-fixAmount)需要fixAmount为int类型,因此
//(1)tool.mutable().attemptDamageItem(-(expectation as int))直接匹配int参数修复会导致期望向下取整,函数完全变成分段函数且失去统计学意义;
//(2)将期望当作随机数概率计算并使用attemptDamageItem(-1)调用修复在钢叶很多时会触及上限,导致每秒最多修复20点耐久;
//此处使用int向下取整找出每tick整数部分的固定修复值,并将小数部分进行随机数处理用以判定是否执行修复(该处修复期望便是小数的值),从而达到
// 总期望=固定修复值+概率修复部分期望
//的期望达成效果
fixAmount += (expectation as int);
if(entity.world.random.nextDouble()<=( expectation - (fixAmount as double) )) fixAmount+=1;
tool.mutable().attemptDamageItem(-fixAmount);
}
}
}
};
thistrait.register();
【教程已无限期暂停更新!】