The Lepidopterist Villager

Last week I added the Butterfly Breeder. This week I used the new block as a job block to create a new villager: the Lepidopterist. This villager will give access to trades around butterflies, giving players a new method of finding new species.

A Point of Interest


Before we can create a new villager type, we need to have a job block. The Butterfly Feeder I created last week can serve as our job block for the Lepidopterist Villager. Before we can use the block, however, we need to add the block as a Point of Interest that the villager’s AI will respond to. To do this, I implemented a PoiTypeRegistry that registers the block.

/**
 * Register POIs for use by the AI.
 */
public class PoiTypeRegistry {

    // An instance of a deferred registry we use to register.
    private final DeferredRegister<PoiType> deferredRegister;

    // The lepidopterist's job block.
    private RegistryObject<PoiType> lepidopterist;

    /**
     * Construction
     * @param modEventBus The event bus to register with.
     */
    public PoiTypeRegistry(IEventBus modEventBus) {
        this.deferredRegister = DeferredRegister.create(ForgeRegistries.POI_TYPES, ButterfliesMod.MOD_ID);
        this.deferredRegister.register(modEventBus);
    }

    /**
     * Register the POI types.
     * @param blockRegistry The block registry.
     */
    public void initialise(BlockRegistry blockRegistry) {
        lepidopterist =deferredRegister.register("lepidopterist",
                () -> new PoiType(getBlockStates(blockRegistry.getButterflyFeeder()), 1, 1));
    }

    /**
     * Accessor to the lepidopterist POI.
     * @return The POI Type.
     */
    public RegistryObject<PoiType> getLepidopterist() {
        return lepidopterist;
    }

    /**
     * Helper method to get a set of block states.
     * @param block The block to get the block states for.
     * @return The set of block states.
     */
    private Set<BlockState> getBlockStates(RegistryObject<Block> block) {
        return ImmutableSet.copyOf(block.get().getStateDefinition().getPossibleStates());
    }
}

This follows the pattern I’ve been using for the other registries of late, with a constructor that registers the deferredRegister, an initialise method to register the POI, and an accessor so the POI can be used outside this class. The actual construction and initialisation happens in the ButterfliesMod class.

This only registers the POI Type, however. To register the POI as a job site we also need to add a JSON file to our data, under resources/data/minecraft/tags/point_of_interest_type/. Creating acquirable_job_site.json allows us to modify the available job sites.

{
  "values": [
    "butterflies:lepidopterist"
  ]
}

Profession


Now that we have a POI Type registered as a Job Site, we can create our actual profession. This is also done using a registry, so I create a simple class that registers a new profession using the new job site.

/**
 * Register professions to be used by villagers.
 */
public class VillagerProfessionRegistry {

    // An instance of a deferred registry we use to register.
    private final DeferredRegister<VillagerProfession> deferredRegister;

    // The lepidopterist profession.
    private RegistryObject<VillagerProfession> lepidopterist;

    /**
     * Construction
     * @param modEventBus The event bus to register with.
     */
    public VillagerProfessionRegistry(IEventBus modEventBus) {
        this.deferredRegister = DeferredRegister.create(ForgeRegistries.VILLAGER_PROFESSIONS, ButterfliesMod.MOD_ID);
        this.deferredRegister.register(modEventBus);
    }

    /**
     * Register the professions.
     * @param poiTypeRegistry The POI Type registry.
     */
    public void initialise(PoiTypeRegistry poiTypeRegistry) {
        lepidopterist = deferredRegister.register("lepidopterist",
                () -> new VillagerProfession(
                        "lepidopterist",
                        x -> x.get() == poiTypeRegistry.getLepidopterist().get(),
                        x -> x.get() == poiTypeRegistry.getLepidopterist().get(),
                        ImmutableSet.of(),
                        ImmutableSet.of(),
                        SoundEvents.VILLAGER_WORK_CLERIC));
    }

    /**
     * Accessor to the lepidopterist profession.
     * @return The POI Type.
     */
    public RegistryObject<VillagerProfession> getLepidopterist() {
        return lepidopterist;
    }
}

The new profession will need a localisation string so that it displays right in-game. This is easily done by just adding a new string to our en_us.json.

  "entity.minecraft.villager.butterflies.lepidopterist": "Lepidopterist"

The final touch is to add some textures for the new villager. I used GIMP for this, though that’s probably overkill. You may find it easier to use an editor specifically designed for pixel art. I tried to make the texture look close to vanilla, but also incorporating the butterfly net and some flowers to show that they’re into butterflies.

It’s important not to forget zombie villagers here. Technically they could have a different texture, but for now I’m just placing the same texture in these two locations:

  • /resources/assets/textures/entity/villager/profession/
  • /resources/assets/textures/entity/zombie_villager/profession/

I like the way they look in game now.

Trades


Of course, what is a villager without trades? Trades are added by responding to an event, but before I do this I create two helper classes. These aren’t my code – you’ll find variations on these all over GitHub. If you’re creating your own villager with your own trades you might want to adjust these classes slightly.

The first class creates a Buying trade, where the villager will give the player emeralds in exchange for a number of items. You can specify the maximum number of trades and the experience awarded for the trade as well.

/**
 * Represents an offer to buy an item.
 */
public class BuyingItemTrade implements VillagerTrades.ItemListing {
    private final Item wantedItem;
    private final int count;
    private final int maxUses;
    private final int xpValue;
    private final float priceMultiplier;

    /**
     * Construction.
     * @param wantedItem The item the trader wants.
     * @param countIn The number of items the trader wants.
     * @param maxUsesIn The maximum number of trades.
     * @param xpValueIn The XP awarded for completing the trade.
     */
    public BuyingItemTrade(ItemLike wantedItem,
                           int countIn,
                           int maxUsesIn,
                           int xpValueIn) {
        this.wantedItem = wantedItem.asItem();
        this.count = countIn;
        this.maxUses = maxUsesIn;
        this.xpValue = xpValueIn;
        this.priceMultiplier = 0.05F;
    }

    @Override
    public MerchantOffer getOffer(@NotNull Entity trader,
                                  @NotNull RandomSource rand) {
        ItemStack stack = new ItemStack(this.wantedItem, this.count);
        return new MerchantOffer(stack,
                new ItemStack(Items.EMERALD),
                this.maxUses,
                this.xpValue,
                this.priceMultiplier);
    }
}

The second class specifies a Selling Item Trade, where items can be bought from the villager for a number of emeralds.

/**
 * Creates a trade for selling.
 */
public class SellingItemTrade implements VillagerTrades.ItemListing {
    private final ItemStack givenItem;
    private final int emeraldCount;
    private final int sellingItemCount;
    private final int maxUses;
    private final int xpValue;
    private final float priceMultiplier;

    /**
     * Constructor
     * @param givenItem The item to sell.
     * @param emeraldCount The base cost in emeralds.
     * @param sellingItemCount The number of items to sell.
     * @param xpValue The XP awarded to the villager on a successful trade.
     */
    public SellingItemTrade(ItemLike givenItem,
                            int emeraldCount,
                            int sellingItemCount,
                            int xpValue) {
        this(new ItemStack(givenItem), emeraldCount, sellingItemCount, 12, xpValue);
    }

    /**
     * Constructor
     * @param givenItem The item to sell.
     * @param emeraldCount The base cost in emeralds.
     * @param sellingItemCount The number of items to sell.
     * @param maxUses The maximum number of trades.
     * @param xpValue The XP awarded to the villager on a successful trade.
     */
    public SellingItemTrade(ItemStack givenItem,
                            int emeraldCount,
                            int sellingItemCount,
                            int maxUses,
                            int xpValue) {
        this(givenItem, emeraldCount, sellingItemCount, maxUses, xpValue, 0.2F);
    }

    /**
     * Constructor
     * @param givenItem The item to sell.
     * @param emeraldCount The base cost in emeralds.
     * @param sellingItemCount The number of items to sell.
     * @param maxUses The maximum number of trades.
     * @param xpValue The XP awarded to the villager on a successful trade.
     * @param priceMultiplier The amount the price is modified by annoying or
     *                        helping the villager.
     */
    public SellingItemTrade(ItemStack givenItem,
                            int emeraldCount,
                            int sellingItemCount,
                            int maxUses,
                            int xpValue,
                            float priceMultiplier) {
        this.givenItem = givenItem;
        this.emeraldCount = emeraldCount;
        this.sellingItemCount = sellingItemCount;
        this.maxUses = maxUses;
        this.xpValue = xpValue;
        this.priceMultiplier = priceMultiplier;
    }

    /**
     * Get an offer for the trader to use.
     * @param trader The trader requesting the offer.
     * @param rand The RNG.
     * @return The offer to the player.
     */
    @Override
    public MerchantOffer getOffer(@NotNull Entity trader,
                                  @NotNull RandomSource rand) {
        return new MerchantOffer(new ItemStack(Items.EMERALD, this.emeraldCount),
                new ItemStack(this.givenItem.getItem(), this.sellingItemCount),
                this.maxUses,
                this.xpValue,
                this.priceMultiplier);
    }
}

With these two classes in the project, it’s much easier to set up the trades. To set up the trades we need to listen for the VillagerTradesEvent on the Forge Event Bus. I came up with a quick design for the available trades and set about adding them to the villager.

/**
 * Listens for events based around villagers.
 */
public class VillageEventListener {

    // Registries.
    private final ItemRegistry itemRegistry;
    private final VillagerProfessionRegistry villagerProfessionRegistry;

    /**
     * Construction
     * @param forgeEventBus The event bus to register with.
     */
    public VillageEventListener(IEventBus forgeEventBus,
                                ItemRegistry itemRegistry,
                                VillagerProfessionRegistry villagerProfessionRegistry) {
        forgeEventBus.register(this);
        forgeEventBus.addListener(this::onVillagerTrades);

        this.itemRegistry = itemRegistry;
        this.villagerProfessionRegistry = villagerProfessionRegistry;
    }

    /**
     * Used to add/modify trades to professions.
     * @param event The event information.
     */
    private void onVillagerTrades(VillagerTradesEvent event) {
        if (event.getType() == villagerProfessionRegistry.getLepidopterist().get()) {
            Int2ObjectMap<List<VillagerTrades.ItemListing>> trades = event.getTrades();

            Collection<ButterflyData> butterflies = ButterflyData.getButterflyDataCollection();

            List<VillagerTrades.ItemListing> tradesLevel1 = trades.get(1);
            List<VillagerTrades.ItemListing> tradesLevel2 = trades.get(2);
            List<VillagerTrades.ItemListing> tradesLevel3 = trades.get(3);
            List<VillagerTrades.ItemListing> tradesLevel4 = trades.get(4);
            List<VillagerTrades.ItemListing> tradesLevel5 = trades.get(5);

            tradesLevel1.add(new SellingItemTrade(itemRegistry.getEmptyButterflyNet().get(), 5, 1, 1));
            tradesLevel3.add(new SellingItemTrade(itemRegistry.getSilk().get(), 32, 8, 10));

            List<RegistryObject<Item>> bottledButterflies = itemRegistry.getBottledButterflies();
            List<RegistryObject<Item>> bottledCaterpillars = itemRegistry.getBottledCaterpillars();
            List<RegistryObject<Item>> butterflyEggs = itemRegistry.getButterflyEggs();
            List<RegistryObject<Item>> butterflyScrolls = itemRegistry.getButterflyScrolls();
            List<RegistryObject<Item>> caterpillars = itemRegistry.getCaterpillars();

            for (ButterflyData butterfly : butterflies) {
                if (butterfly.type() != ButterflyData.ButterflyType.SPECIAL) {
                    int i = butterfly.butterflyIndex();
                    switch (butterfly.rarity()) {
                        case COMMON:
                            tradesLevel1.add(new BuyingItemTrade(butterflyEggs.get(i).get(), 15, 16, 2));
                            tradesLevel1.add(new SellingItemTrade(butterflyEggs.get(i).get(), 6, 1, 1));

                            tradesLevel2.add(new SellingItemTrade(bottledCaterpillars.get(i).get(), 10, 1, 5));
                            tradesLevel2.add(new SellingItemTrade(butterflyScrolls.get(i).get(), 15, 1, 5));
                            tradesLevel2.add(new BuyingItemTrade(caterpillars.get(i).get(), 10, 12, 10));
                            tradesLevel2.add(new SellingItemTrade(caterpillars.get(i).get(), 8, 1, 5));

                            tradesLevel3.add(new SellingItemTrade(bottledButterflies.get(i).get(), 15, 1, 10));
                            break;

                        case UNCOMMON:
                            tradesLevel2.add(new BuyingItemTrade(butterflyEggs.get(i).get(), 15, 12, 1));
                            tradesLevel2.add(new SellingItemTrade(butterflyEggs.get(i).get(), 8, 1, 1));

                            tradesLevel3.add(new SellingItemTrade(bottledCaterpillars.get(i).get(), 15, 1, 10));
                            tradesLevel3.add(new SellingItemTrade(butterflyScrolls.get(i).get(), 20, 1, 10));
                            tradesLevel3.add(new BuyingItemTrade(caterpillars.get(i).get(), 8, 12, 20));
                            tradesLevel3.add(new SellingItemTrade(caterpillars.get(i).get(), 10, 1, 10));

                            tradesLevel4.add(new SellingItemTrade(bottledButterflies.get(i).get(), 20, 1, 15));
                            break;

                        case RARE:
                            tradesLevel3.add(new BuyingItemTrade(butterflyEggs.get(i).get(), 10, 1, 20));
                            tradesLevel3.add(new SellingItemTrade(butterflyEggs.get(i).get(), 10, 1, 10));

                            tradesLevel4.add(new SellingItemTrade(bottledCaterpillars.get(i).get(), 20, 1, 15));
                            tradesLevel4.add(new SellingItemTrade(butterflyScrolls.get(i).get(), 32, 1, 15));
                            tradesLevel4.add(new BuyingItemTrade(caterpillars.get(i).get(), 6, 1, 30));
                            tradesLevel4.add(new SellingItemTrade(caterpillars.get(i).get(), 15, 1, 15));

                            tradesLevel5.add(new SellingItemTrade(bottledButterflies.get(i).get(), 32, 1, 30));
                            break;
                    }
                }
            }
        }
    }
}

The code for this is a bit more complex than it would be for a normal villager due to the sheer number of items available. I wanted to make sure that players could use these villagers to access rare butterflies they may have difficulty finding in specific biomes. So we iterate over every available butterfly and add their trades based on their rarity. Doing it this way means that future butterflies will get added to the trades automatically as well, so it means we don’t need to worry about changing this in the future.

Now, players will be able to level up their Lepidopterist Villagers and gain access to rarer butterflies!

Appendix – Checklists


When adding things to Minecraft it’s very easy to forget a step here or there. So I started to create check lists to help me remember what needs to be done whenever I add something new. I’ve decided to publish these on the site on the off chance that others may benefit from them.

They aren’t development diaries so I’ve tried to keep them mostly mod neutral, but they aren’t full tutorials either. They’re just meant to act as reminders for things you may have forgotten whenever you add something new to the game, be it a new block, a new registry, or a new profession.

I’ll be adding to and updating these as time goes on, so check them out. They may prove useful!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.