战利品表
战利品表(Loot Table)可以非常容易地以指定的随机分布生成战利品。它在原版中用于生成随机的宝箱战利品以及生物掉落。
原版的Wiki非常详细地描述了战利品表的JSON格式,所以这篇文章将会着重于在Mod中操作战利品表的代码。如果想要完全理解这篇文章,请在阅读本文之前先仔细阅读之前提到的Wiki页。
注册一个Mod战利品表
如果想要让Minecraft加载你的战利品表,可以调用 LootTableList.register(new ResourceLocation("modid", "loot_table_name")),这将会解析并加载 /assets/modid/loot_tables/loot_table_name.json。你可以在preInit、init和postInit三个阶段中任意一个阶段调用这个方法。你也可以将战利品表整理到多个文件夹内。
提示
战利品表中的战利品随机池(Loot Pool)必须要附加一个 name 标签从而能够在表中唯一识别出这个随机池。通常情况下是使用这个随机池包含的物品种类来命名的。
如果你对多个条目(Entry)使用了同一个 name 标签(比如同一个物品但施加不同的函数(Function),那么你必须要给每一个条目一个 entryName 标签,以唯一识别出随机池中的条目。对于那些没有冲突的 name 标签,entryName 将会自动设置为 name 的值。
这些附加的要求都是由Forge实施的,用于在加载的时候使用 LootTableLoadEvent 辅助战利品表的修改(见下)。
注册自定义对象
除了原版提供的那些,你也可以自己注册你自己的战利品条件(Condition)、战利品函数以及实体属性(Entity Properties)。
实体属性仅仅用于 minecraft:entity_properties 这个战利品条件,它可以用来检测参与掠夺(Looting)的实体(被掠夺的实体或者是杀手(Killer))是否拥有特定的属性。原版中唯一的属性是 minecraft:on_fire。
这三个条目的注册非常相似,只需要分别调用 LootConditionManager.registerCondition,LootFunctionManager.registerFunction() 或 EntityPropertyManager.registerProperty() 就可以了。
这些方法都需要传入一个 Serializer 的实例,构建它需要对象的ID(ResourceLocation)以及代码中实现这些行为的 Class —— LootCondition,LootFunction 或 EntityProperty。
由于你注册了JSON的序列化器(Serializer)以及反序列化器(Deserializer),你可以添加自定义的字段到条件、函数和属性中。见原版 net.minecraft.world.storage.loot.{conditions, functions, properties} 包中的实现。
接下来,为了能够使用你的条件、函数、和条件,你需要将它的注册表名传入 Serializer 的构造器。下面是一个战利品条目的例子:
{
"type": "item",
"name": "mymod:myitem",
"conditions": [
{
"condition": "mymod:mycondition",
"foo": 1, // 可以添加自定义的参数到反序列化器中
},
{
"condition": "minecraft:entity_properties",
"entity": "this",
"properties": {
"mymod:my_property": { // 右边的结构完全取决于反序列化器
"bar": 2
}
}
}
],
"functions": [
{
"function": "mymod:myfunction",
"foobar": 3 // 可以添加自定义的参数到反序列化器中
}
]
}
修改原版的战利品
综述
你不仅可以添加你自己的战利品表、条件、函数和实体属性,你也可以在加载的时候修改这些项目。
提示
原版允许用户在世界的存档目录中添加自己的战利品表来覆盖游戏(以及Mod)自己的表。这些都被视为是配置(Config)文件,并且设计上不能够被以下的方法所修改。
运行时修改战利品表的入口点是 LootTableLoadEvent,它会在每个表加载的时候触发。在这里,你可以通过名称查询以及移除随机池,或者添加一个 Loot Pool 的实例。这就是为什么Mod中的战利品随机池必须拥有一个名字。
你可能会在想,我们如何才能修改原版的战利品表呢,他们并没有名字。Forge通过对所有的原版战利品表生成名字来解决这个问题。第一个随机池被命名为 main,因为大部分的表只有一个随机池。之后的随机池会根据位置命名:第二个随机池是 pool1,第三个是 pool2,以此类推。删除一个随机池并不会将名字移到别的随机池中。
在每一个 LootPool 中,你可以修改随机池的Roll以及Bonus Roll(战利品表将会调用这个随机池多少次),以及通过名字查询和删除条目,或者添加 LootEntry 的名字。
和随机池一样,条目也需要有唯一的名字供获取和移除使用。Forge通过增加一个隐藏的 entryName 字段到所有的战利品条目中来解决这个问题。如果条目的 name 字段在随机池中是唯一的,那么 entryName 将会自动设置为 name。如果不是的话,则需要手动在Mod的战利品表条目中添加一个,原版的条目则会自动生成。对于每一次的重复,数字会递增。比如说,如果原版中有三个条目,每个都是 name: "minecraft:stick",那么生成的三个 entryName 标签则会是 minecraft:stick,minecraft:stick#0 和 minecraft:stick#1。同样,删除一个条目并不会将名字移到别的条目中。
提示
你必须要在战利品表的 LootTableLoadEvent 中完成所有对该战利品表所需的改动,之后的任何改动将会被安全检查禁止,如果安全检查被跳过的话则会造成未定义的行为。
添加地牢战利品
接下来是修改原版战利品的一个非常常见的案例:添加地牢的物品生成、
首先需要监听要修改的战利品表的事件:
@SubscribeEvent
public void lootLoad(LootTableLoadEvent evt) {
if (evt.getName().toString().equals("minecraft:chests/simple_dungeon")) {
// 使用 evt.getTable() 来获取战利品表
}
}
在这里,我们添加到的是潜在的生成,但我们不想干涉已有随机池的权重。解决这个问题最灵活也是最简单的方法是添加另一个随机池,其中只有一个战利品条目,引用你自己的战利品表,战利品条目可以递归提取自完全不同的战利品表。
比如说,你的Mod可能会包含一个 /assets/mymod/loot_tables/inject/simple_dungeon.json:
{
"pools": [
{
"name": "main",
"rolls": 1,
"entries": [
{
"type": "item",
"name": "minecraft:nether_star",
"weight": 40
},
{
"type": "empty",
"weight": 60
}
]
}
]
}
提示
你仍然需要通过 LootTableList.register() 来注册这个表。
这样子战利品条目和随机池就被创建并添加了,地牢箱子将会有一个新的随机池,60%几率什么都不产生,40%的几率产生一个下界之星。
LootEntry entry = new LootEntryTable(new ResourceLocation("mymod:inject/simple_dungeon"), <weight>, <quality>, <conditions>, <entryName>); // weight 在这里不重要,因为随机池里只有一个条目。其它的参数按你的喜好来设置。
LootPool pool = new LootPool(new LootEntry[] {entry}, <conditions>, <rolls>, <bonusRolls>, <name>); // 其它的参数按你的喜好来设置。
evt.getTable().addPool(pool);
当然,如果你添加的战利品不能在这之前决定,你也可以利用类似上面的调用自己构造并添加 LootPool 和 LootEntry 的实现到事件处理器中。
这种方法实际的例子可以在Botania中找到。事件管理器能在这里找到,注入的战利品表在这里。
改变生物掉落
EntityLiving 的子类自动支持死亡时从战利品表中提取战利品。你可以复写 getLootTable,将返回值改为想要的战利品表的 ResourceLocation。它将是这个生物的默认战利品表,通过设置 deathLootTable 的值,不论是你的Mod还是别人的Mod中的生物,你都可以对单独一个实体复写它的战利品表。
在代码中生成战利品
偶尔你也可能会想在自己的代码中从战利品表生成 ItemStack。
首先,你需要获取战利品表本身(你需要能获取到一个 World):
LootTable table = this.world.getLootTableManager().getLootTableFromLocation(new ResourceLocation("mymod:my_table")); // 解析至 /assets/mymod/loot_tables/my_table.json
接下来使用提供的 LootContextBuilder 创建一个 LootContext,它封装了掠夺的上下文(Context),比如杀手,掠夺者的幸运(Luck)、以及伤害源。
LootContext ctx = new LootContext.Builder(world)
.withLuck(...) // 调整幸运值,通常是 EntityPlayer.getLuck()
.withLootedEntity(...) // 被掠夺的实体
.withPlayer(...) // 将玩家设置为掠夺者
.withDamageSource(...) // 打击(Killing Blow)和非玩家杀手
.build();
最后获取一个 ItemStack 的集合:
List<ItemStack> stacks = table.generateLootForPools(world.rand, ctx);
或者填充物品栏(Inventory):
table.fillInventory(iinventory, world.rand, ctx);
提示
目前只能使用 IInventory