Butterfly Nets: Gotta Catch ‘Em All

I mentioned in my last article that I wanted to add a collectible element to my butterfly mod. In order to do that we need a way to catch said butterflies. So the next step is to implement a butterfly net. With the latest change we can now craft butterfly nets that can be used to catch (and release) ’em all.

Release Butterfly

Before we implement the butterfly net itself, we need a way to spawn butterflies if the player decides to release them. This means we can re-use the same butterfly net rather than having to keep crafting new ones.

We do this by adding a new method to the butterfly class. It takes the player that is releasing the butterfly, an entityId so we know what kind of butterfly to spawn, and the position we want to release the butterfly.

    /**
     * Used to release a butterfly from an item back into the world.
     * @param level The current level.
     * @param player The player releasing the butterfly.
     * @param entityId The type of butterfly to release.
     * @param position The current position of the player.
     */
    public static void release(@NotNull Player player,
                               String entityId,
                               BlockPos position) {
    }

We only need to actually create the butterfly on the server. Otherwise we just play a sound so the player knows an action has been performed.

    /**
     * Used to release a butterfly from an item back into the world.
     * @param level The current level.
     * @param player The player releasing the butterfly.
     * @param entityId The type of butterfly to release.
     * @param position The current position of the player.
     */
    public static void release(@NotNull Player player,
                               String entityId,
                               BlockPos position) {
        
        Level level = player.level;
        if (level instanceof ServerLevel) {
        } else {
            player.playSound(SoundEvents.PLAYER_ATTACK_WEAK, 1F, 1F);
        }
    }

We offset the position a little so the butterfly spawns in front of the player. This gives the player more obvious feedback. If the butterfly spawns behind the player, they may think that it has disappeared completely.

    /**
     * Used to release a butterfly from an item back into the world.
     * @param level The current level.
     * @param player The player releasing the butterfly.
     * @param entityId The type of butterfly to release.
     * @param position The current position of the player.
     */
    public static void release(@NotNull Player player,
                               String entityId,
                               BlockPos position) {
        
        Level level = player.level;
        if (level instanceof ServerLevel) {

            //  Move the target position slightly in front of the player
            Vec3 lookAngle = player.getLookAngle();
            position = position.offset((int) lookAngle.x, (int) lookAngle.y + 1, (int) lookAngle.z);

        } else {
            player.playSound(SoundEvents.PLAYER_ATTACK_WEAK, 1F, 1F);
        }
    }

Next, we find the kind of entity we want to create from the Forge registry. We add a check to ensure that the entity created actually is a butterfly.

    /**
     * Used to release a butterfly from an item back into the world.
     * @param level The current level.
     * @param player The player releasing the butterfly.
     * @param entityId The type of butterfly to release.
     * @param position The current position of the player.
     */
    public static void release(@NotNull Player player,
                               String entityId,
                               BlockPos position) {
        
        Level level = player.level;
        if (level instanceof ServerLevel) {

            //  Move the target position slightly in front of the player
            Vec3 lookAngle = player.getLookAngle();
            position = position.offset((int) lookAngle.x, (int) lookAngle.y + 1, (int) lookAngle.z);

            ResourceLocation key = new ResourceLocation(entityId);
            EntityType<?> entityType = ForgeRegistries.ENTITY_TYPES.getValue(key);
            if (entityType != null) {
                Entity entity = entityType.create(player.level);
                if (entity instanceof Butterfly butterfly) {

                }
            }
        } else {
            player.playSound(SoundEvents.PLAYER_ATTACK_WEAK, 1F, 1F);
        }
    }

Finally, we move the butterfly to the position we want and finalise the spawn. We use the setPlacedByPlayer() to ensure that butterflies spawned like this don’t disappear. This can allow for players creating a lepidopterarium in the world, if they want to have a physical collection of butterflies.

    /**
     * Used to release a butterfly from an item back into the world.
     * @param level The current level.
     * @param player The player releasing the butterfly.
     * @param entityId The type of butterfly to release.
     * @param position The current position of the player.
     */
    public static void release(@NotNull Player player,
                               String entityId,
                               BlockPos position) {
        
        Level level = player.level;
        if (level instanceof ServerLevel) {

            //  Move the target position slightly in front of the player
            Vec3 lookAngle = player.getLookAngle();
            position = position.offset((int) lookAngle.x, (int) lookAngle.y + 1, (int) lookAngle.z);

            ResourceLocation key = new ResourceLocation(entityId);
            EntityType<?> entityType = ForgeRegistries.ENTITY_TYPES.getValue(key);
            if (entityType != null) {
                Entity entity = entityType.create(player.level);
                if (entity instanceof Butterfly butterfly) {

                    butterfly.moveTo(position.getX() + 0.45D,
                            position.getY() + 0.2D,
                            position.getZ() + 0.5D,
                            0.0F, 0.0F);
                    
                    butterfly.finalizeSpawn((ServerLevel) level,
                            level.getCurrentDifficultyAt(player.getOnPos()),
                            MobSpawnType.NATURAL,
                            null,
                            null);

                    butterfly.setPlacedByPlayer();
                    level.addFreshEntity(butterfly);
                }
            }
        } else {
            player.playSound(SoundEvents.PLAYER_ATTACK_WEAK, 1F, 1F);
        }
    }

Now that we have a method of creating a butterfly, we can move on to writing the code for the butterfly net.

Butterfly Net Item

To start with, we need to create a class that extends from minecraft’s Item class. We create a constant NAME that can be used to register the item later.

public class ButterflyNetItem extends Item {

    public static final String NAME = "butterfly_net";

    /**
     * Construction
     * @param properties The item properties.
     */
    public ButterflyNetItem(Properties properties) {
        super(properties);
    }
}

We need to be able to catch a butterfly. We do this by overriding onLeftClickEntity which is called when a player clicks on an entity with the item in hand. In our case, if the player clicks a butterfly when they are holding a butterfly net, they will catch the butterfly.

CompoundTag is the NBT data for the item. NBT data is saved to the world so they will still be there if we leave and reload the world. We use this to save the butterfly’s entity ID, and mark the net as being full using the “CustomModelData” tag. The “CustomModelData” will be used in the item’s model to change its appearance when it is full.

Before returning from the method, we play a sound to give extra feedback to the player.

    public static final String ENTITY_ID = "EntityId";
    public static final String CUSTOM_MODEL_DATA = "CustomModelData";

    /**
     * If we left-click on a butterfly with an empty net, the player will catch the butterfly.
     * @param stack  The Item being used
     * @param player The player that is attacking
     * @param entity The entity being attacked
     * @return TRUE if the left-click action is consumed.
     */
    @Override
    public boolean onLeftClickEntity(ItemStack stack, Player player, Entity entity) {
        if (entity instanceof Butterfly) {
            CompoundTag tag = stack.getOrCreateTag();
            if (!tag.contains(CUSTOM_MODEL_DATA) ||
                !tag.contains(ENTITY_ID)) {

                tag.putInt(CUSTOM_MODEL_DATA,1);
                tag.putString(ENTITY_ID, Objects.requireNonNull(entity.getEncodeId()));
                entity.discard();

                player.playSound(SoundEvents.PLAYER_ATTACK_SWEEP, 1F, 1F);

                return true;
            }
        }

        return super.onLeftClickEntity(stack, player, entity);
    }

After a player catches a butterfly, they can release it by right-clicking. We override the use() method to do this, which is called when a player right-clicks with an item in hand. We then check the NBT data using CompoundTag, and if there is a butterfly in the net we call the release() method we implemented above.

    /**
     * Right-clicking with a full net will release the net.
     * @param level The current level.
     * @param player The player holding the net.
     * @param hand The player's hand.
     * @return The result of the action, if any.
     */
    @Override
    @NotNull
    public  InteractionResultHolder<ItemStack> use(@NotNull Level level,
                                                   @NotNull Player player,
                                                   @NotNull InteractionHand hand) {

        ItemStack stack = player.getItemInHand(hand);
        CompoundTag tag = stack.getOrCreateTag();
        if (tag.contains("EntityId")) {
            String entityId = tag.getString("EntityId");
            Butterfly.release(level, player, entityId, player.blockPosition());
            tag.remove("CustomModelData");
            tag.remove("EntityId");

            return InteractionResultHolder.success(stack);
        }

        return super.use(level, player, hand);
    }

We can now have a full butterfly net or an empty one, but there’s still a problem. Players should know what kind of butterfly has been caught. To do this we can add a tooltip to the item description by overriding appendHoverText() and adding a new MutableComponent to the list of components.

We construct a string ID to use for translation. The mod only has English right now, but this future proofs things if we ever do any translations.

We modify the MutableComponent‘s style so that the text will appear red and in italics. This helps to distinguish it from the actual name of the item.

    /**
     * Adds some helper text that tells us what butterfly is in the net (if any).
     * @param stack The item stack.
     * @param level The current level.
     * @param components The current text components.
     * @param tooltipFlag Is this a tooltip?
     */
    @Override
    public void appendHoverText(@NotNull ItemStack stack,
                                @Nullable Level level,
                                @NotNull List<Component> components,
                                @NotNull TooltipFlag tooltipFlag) {
        CompoundTag tag = stack.getOrCreateTag();
        if (tag.contains(ENTITY_ID)) {
            String translatable = "item." + tag.getString(ENTITY_ID).replace(':', '.');
            MutableComponent newComponent = Component.translatable(translatable);
            Style style = newComponent.getStyle().withColor(TextColor.fromLegacyFormat(ChatFormatting.DARK_RED)).withItalic(true);
            newComponent.setStyle(style);
            components.add(newComponent);
        }

        super.appendHoverText(stack, level, components, tooltipFlag);
    }

Now players will be able to hover over a butterfly net in their inventory and see what kind of butterfly is in there.

Tooltip showing there is a monarch butterfly in the net

Item Registry

To have the item appear in the game we need to add it to the Item Registry. We have already done this with butterfly eggs, and adding the butterfly net is done in the same way.

    //  Butterfly net - Used to catch butterflies
    public static final RegistryObject<Item> BUTTERFLY_NET = INSTANCE.register(ButterflyNetItem.NAME,
            () -> new ButterflyNetItem(new Item.Properties().stacksTo(1)));

We can also add it to the creative menu by updating our registerCreativeTabContents() method. We add this one to the “Tools & Utilities” tab, rather than the “Spawn Eggs” tab.

    @SubscribeEvent
    public static void registerCreativeTabContents(CreativeModeTabEvent.BuildContents event) {
        if (event.getTab() == CreativeModeTabs.TOOLS_AND_UTILITIES) {
            event.accept(BUTTERFLY_NET);
        }
    }

Now we will be able to use the item in game, and can access it via the creative inventory tab.

Butterfly Net in the creative inventory.

Model

At this stage if you load the game with the mod installed, the butterfly net will have no texture. We need to add a model to the mod’s data to tell the game what textures to use. We have two textures: one to represent an empty net, and the other for a full net.

We store the textures under /resources/assets/<MOD_ID>/textures. To tell the game to use these textures, we need a model. We have used these for spawn eggs already, but this model will be a bit different. We need to tell the game which one to use based on the “CustomModelData” NBT we used in the item class.

Under /resources/assets/<MOD_ID>/models/item we add butterfly_net.json. Since the name matches or items resource ID it will automatically be used for the butterfly net.

{
  "parent": "item/handheld_rod",
  "textures": {
    "layer0": "butterflies:item/butterfly_net/butterfly_net"
  },
  "overrides": [
    { "predicate": { "custom_model_data": 1 }, "model": "butterflies:item/butterfly_net_full" }
  ]
}

This tells the game to use the empty net texture for the new item. We also add an override based on the custom_model_data. What this is saying is that if the item has a “CustomModelData” tag, and its value is set to “1”, then use a different model. In this case butterfly_net_full.json.

{
  "parent": "item/handheld_rod",
  "textures": {
    "layer0": "butterflies:item/butterfly_net/butterfly_net_full"
  }
}

All this does is tell the game to use the alternative “full” texture instead.

Recipe

We can access the butterfly net through commands or using the creative tab, but if we are playing survival mode without cheats we have no way to craft the butterfly net. To allow for this we need to add a recipe in the data.

Recipes are places under /resources/data/<MOD_ID>/recipes. They’re a collection of JSON files that tell the game how we can craft an item. This recipe is a 3×3 pattern-based recipe similar to a fishing rod, except it uses 3 strings instead of just two.

{
  "type": "minecraft:crafting_shaped",
  "pattern": [
    "  /",
    " /s",
    "/ss"
  ],
  "key": {
    "#": {
      "item": "minecraft:stick"
    },
    "X": {
      "item": "minecraft:string"
    }
  },
  "result": {
    "item": "butterflies:butterfly_net"
  }
}

The pattern shows how the items should be arranged in the 3×3 grid. The key tells us which items each symbol represents.

Crafting the butterfly net.

This will allow us to craft the item, but it won’t show in the recipe book. For the player to be able to unlock the recipe we need to add a recipe advancement. These are stored under /resources/data/<MOD_ID>/advancements/recipes/. In our case, it is a tool recipe, so we put it under the tools folder here.

{
  "parent": "minecraft:recipes/root",
  "rewards": {
    "recipes": [
      "butterflies:butterfly_net"
    ]
  },
  "criteria": {
    "has_string": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "items": [
              "minecraft:string"
            ]
          }
        ]
      }
    },
    "has_the_recipe": {
      "trigger": "minecraft:recipe_unlocked",
      "conditions": {
        "recipe": "butterflies:butterfly_net"
      }
    }
  },
  "requirements": [
    [
      "has_string",
      "has_the_recipe"
    ]
  ]
}

The parent is set to minecraft:recipes/root, telling us this advancement belongs to the recipes group. The reward tells us which recipe we unlock if we meet the criteria. In this case we will unlock the butterfly_net recipe.

For our criteria we have two possible triggers. One detects if the player picks up string. The other checks if the player already has the recipe.

The requirements is an array of arrays. Each element in the outer array is a separate requirement. If we have multiple requirements, then we would have to meet each one to unlock the advancement. In this case we have only one element, so only one requirement.

The inner array lists two criterion. Since they are both within the same element of the outer array, the player only needs to unlock one in order to get the reward (i.e. the recipe).

Advancements

We can add other advancements to make the mod more interesting. In this case we can add advancements for catching a butterfly, and one for catching all 16 butterfly species. To do this we use our own folder under /resources/data/<MOD_ID>/advancements/, in our case butterfly.

The first advancement we need is the root. This creates a new advancement category, separate to any of the vanilla categories. This is a JSON file similar to the advancement that unlocks a recipe.

In this case, we don’t have a reward – the advancement itself is the reward. What we do have is the "display" attribute to describe what the advancement looks like. We tell it to get the icon from an item, in this case the monarch butterfly egg. We provide translation strings for the name and description which we will create later. We set the frame to be a task, and the background to use a stone texture.

We have "show_toast" and "announce_to_chat" set to false, since this advancement just defines the category. This means the advancement won’t be announced to other players when it is achieved.

The "criteria" and "requirements" work the same as for the recipe. In this case the advancement will be unlocked when the player gets a butterfly net.

{
  "display": {
    "icon": {
      "item": "butterflies:monarch"
    },
    "title": {
      "translate": "advancements.butterfly.root.title"
    },
    "description": {
      "translate": "advancements.butterfly.root.description"
    },
    "frame": "task",
    "show_toast": false,
    "announce_to_chat": false,
    "hidden": false,
    "background": "minecraft:textures/gui/advancements/backgrounds/stone.png"
  },
  "criteria": {
    "butterfly_net": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net"
          }
        ]
      }
    }
  },
  "requirements": [
    [
      "butterfly_net"
    ]
  ]
}

The next advancement is awarded the first time the player catches a butterfly. "show_toast" and "announce_to_chat" are both set to true so this one will be announced to other players in the chat. Like the root advancement, this one is unlocked when the player gets a butterfly net, but we also check the NBT data for to see if CustomModelData is set to 1. This means the player needs a net with a butterfly inside, which in survival can only happen by catching a butterfly.

{
  "parent": "butterflies:butterfly/root",
  "display": {
    "icon": {
      "item": "butterflies:butterfly_net"
    },
    "title": {
      "translate": "advancements.butterfly.catch_butterfly.title"
    },
    "description": {
      "translate": "advancements.butterfly.catch_butterfly.description"
    },
    "frame": "task",
    "show_toast": true,
    "announce_to_chat": true,
    "hidden": false
  },
  "criteria": {
    "caught_butterfly": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{CustomModelData:1}"
          }
        ]
      }
    }
  },
  "requirements": [
    [
      "caught_butterfly"
    ]
  ]
}

The last advancement is our “Catch ‘Em All” advancement. We set the NBT data in our icon so that is uses a full net rather than an empty net. We also set the frame to be a "goal" to indicate that this is an advancement the player can choose to work towards. As a goal, we award some experience for completing it.

The criterion use NBT data like the previous advancement, however we use the EntityId tag here instead of CustomModelData. This allows us to create a condition for each butterfly in the mod.

Finally the requirements are a list of arrays. Since each requirement is in an array of its own, a player will have to complete all 16 in order to get this advancement.

{
  "parent": "butterflies:butterfly/catch_butterfly",
  "display": {
    "icon": {
      "item": "butterflies:butterfly_net",
      "nbt": "{CustomModelData:1}"
    },
    "title": {
      "translate": "advancements.butterfly.catch_all_butterflies.title"
    },
    "description": {
      "translate": "advancements.butterfly.catch_all_butterflies.description"
    },
    "frame": "goal",
    "show_toast": true,
    "announce_to_chat": true,
    "hidden": false
  },
  "rewards": {
    "experience": 100
  },
  "criteria": {
    "caught_admiral": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:admiral\"}"
          }
        ]
      }
    },
    "caught_buckeye": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:buckeye\"}"
          }
        ]
      }
    },
    "caught_cabbage": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:cabbage\"}"
          }
        ]
      }
    },
    "caught_chalkhill": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:chalkhill\"}"
          }
        ]
      }
    },
    "caught_clipper": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:clipper\"}"
          }
        ]
      }
    },
    "caught_common": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:common\"}"
          }
        ]
      }
    },
    "caught_emporer": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:emporer\"}"
          }
        ]
      }
    },
    "caught_forester": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:forester\"}"
          }
        ]
      }
    },
    "caught_glasswing": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:glasswing\"}"
          }
        ]
      }
    },
    "caught_hairstreak": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:hairstreak\"}"
          }
        ]
      }
    },
    "caught_heath": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:heath\"}"
          }
        ]
      }
    },
    "caught_longwing": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:longwing\"}"
          }
        ]
      }
    },
    "caught_monarch": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:monarch\"}"
          }
        ]
      }
    },
    "caught_morpho": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:morpho\"}"
          }
        ]
      }
    },
    "caught_rainbow": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:rainbow\"}"
          }
        ]
      }
    },
    "caught_swallowtail": {
      "trigger": "minecraft:inventory_changed",
      "conditions": {
        "items": [
          {
            "item": "butterflies:butterfly_net",
            "nbt": "{EntityId:\"butterflies:swallowtail\"}"
          }
        ]
      }
    }
  },
  "requirements": [
    [
      "caught_admiral"
    ],
    [
      "caught_buckeye"
    ],
    [
      "caught_cabbage"
    ],
    [
      "caught_chalkhill"
    ],
    [
      "caught_clipper"
    ],
    [
      "caught_common"
    ],
    [
      "caught_emporer"
    ],
    [
      "caught_forester"
    ],
    [
      "caught_glasswing"
    ],
    [
      "caught_hairstreak"
    ],
    [
      "caught_heath"
    ],
    [
      "caught_longwing"
    ],
    [
      "caught_monarch"
    ],
    [
      "caught_morpho"
    ],
    [
      "caught_rainbow"
    ],
    [
      "caught_swallowtail"
    ]
  ]
}

Using advancements we can give players direction in what they can do with the mod. We may not have much to do right now, but it’s a good start.

The advancements available in the butterfly mod at the time of writing.

Localisation

We mustn’t forget the localisation strings. In our en_us.json, we need to add translations for all the new item and achievement strings we have added.

  "item.butterflies.butterfly_net": "Butterfly Net",

  "advancements.butterfly.root.title": "Butterflies",
  "advancements.butterfly.root.description": "Fluttering with colour",
  "advancements.butterfly.catch_butterfly.title": "Butterfly Catcher",
  "advancements.butterfly.catch_butterfly.description": "Catch a butterfly with a butterfly net",
  "advancements.butterfly.catch_all_butterflies.title": "Butterfly Collector",
  "advancements.butterfly.catch_all_butterflies.description": "Gotta catch 'em all"

One item string and 3 pairs of advancement strings and our butterfly net is feature complete!

Playtesting

Not quite complete though…

I built the mod and updated it in my single player installation. I loaded into my survival world and built a butterfly net before rushing around trying to find some butterflies to catch.

It was more than frustrating. Finding butterflies was easy, but catching them? It was extremely hard. The butterflies move too fast, and I basically only caught one out of luck. It was not a good experience, at least in my eyes.

So I decided to slow down the butterflies. You can see the details in this change, but the gist of it is that I reduced their speed along the XZ plane to around 32% of what it was originally. They still animate well when flying, and are easier to catch now.

It’s still pretty challenging to catch ’em all, however.

One thought on “Butterfly Nets: Gotta Catch ‘Em All

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.