Laying Butterfly Eggs in Leaves

Butterflies lay eggs that eventually become caterpillars. Caterpillars then feed on leaves until they are fat enough to become a chrysalis. Finally, they morph into a butterfly before breaking out of the chrysalis. The next step for our mod is to implement butterfly eggs.

Butterfly Eggs

To start with we design some butterfly egg icons. These are all based upon actual butterfly eggs. It turns out they come in many shapes and sizes so there is decent variety in their appearance. The only one created from scratch is the rainbow butterfly egg, since this one isn’t a real-life species.

The butterfly leaves item is simple enough to implement. We want to be able to stack the eggs so we will have a different registry entry for each one.

/**
 * An egg that will eventually hatch into a caterpillar.
 */
public class ButterflyEggItem extends Item implements ButterflyContainerItem {

    //  The name this item is registered under.
    public static final String ADMIRAL_NAME = "admiral_egg";
    public static final String BUCKEYE_NAME = "buckeye_egg";
    public static final String CABBAGE_NAME = "cabbage_egg";
    public static final String CHALKHILL_NAME = "chalkhill_egg";
    public static final String CLIPPER_NAME = "clipper_egg";
    public static final String COMMON_NAME = "common_egg";
    public static final String EMPEROR_NAME = "emperor_egg";
    public static final String FORESTER_NAME = "forester_egg";
    public static final String GLASSWING_NAME = "glasswing_egg";
    public static final String HAIRSTREAK_NAME = "hairstreak_egg";
    public static final String HEATH_NAME = "heath_egg";
    public static final String LONGWING_NAME = "longwing_egg";
    public static final String MONARCH_NAME = "monarch_egg";
    public static final String MORPHO_NAME = "morpho_egg";
    public static final String RAINBOW_NAME = "rainbow_egg";
    public static final String SWALLOWTAIL_NAME = "swallowtail_egg";

    private final String entityId;

    /**
     * Construction
     * @param properties The item properties.
     */
    public ButterflyEggItem(String entityId, Item.Properties properties) {
        super(properties);
        this.entityId = entityId;
    }
}

We store the Entity ID so that we can use it to plant an egg for the correct species. Players can plant an egg by right-clicking any leaf block, and we implement this action by overriding the useOn() method.


    /**
     * If used on a leaf block will place the egg.
     * @param context The context of the use action.
     * @return The result of the use action.
     */
    @NotNull
    @Override
    public InteractionResult useOn(@NotNull UseOnContext context) {
        Level level = context.getLevel();
        BlockPos position = context.getClickedPos();

        if (ButterflyLeavesBlock.plantButterflyEgg(level, position, this.entityId)) {
            ItemStack itemStack = context.getItemInHand();
            itemStack.shrink(1);
            return InteractionResult.sidedSuccess(level.isClientSide);
        }

        return super.useOn(context);
    }

In our item registry we can now register each egg with a specific species of butterfly.

    // Butterfly Eggs - Eggs that will eventually hatch into a caterpillar.
    public static final RegistryObject<Item> ADMIRAL_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.ADMIRAL_NAME,
            () -> new ButterflyEggItem(Butterfly.ADMIRAL_NAME, new Item.Properties()));

    public static final RegistryObject<Item> BUCKEYE_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.BUCKEYE_NAME,
            () -> new ButterflyEggItem(Butterfly.BUCKEYE_NAME, new Item.Properties()));

    public static final RegistryObject<Item> CABBAGE_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.CABBAGE_NAME,
            () -> new ButterflyEggItem(Butterfly.CABBAGE_NAME, new Item.Properties()));

    public static final RegistryObject<Item> CHALKHILL_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.CHALKHILL_NAME,
            () -> new ButterflyEggItem(Butterfly.CHALKHILL_NAME, new Item.Properties()));

    public static final RegistryObject<Item> CLIPPER_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.CLIPPER_NAME,
            () -> new ButterflyEggItem(Butterfly.CLIPPER_NAME, new Item.Properties()));

    public static final RegistryObject<Item> COMMON_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.COMMON_NAME,
            () -> new ButterflyEggItem(Butterfly.COMMON_NAME, new Item.Properties()));

    public static final RegistryObject<Item> EMPEROR_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.EMPEROR_NAME,
            () -> new ButterflyEggItem(Butterfly.EMPEROR_NAME, new Item.Properties()));

    public static final RegistryObject<Item> FORESTER_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.FORESTER_NAME,
            () -> new ButterflyEggItem(Butterfly.FORESTER_NAME, new Item.Properties()));

    public static final RegistryObject<Item> GLASSWING_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.GLASSWING_NAME,
            () -> new ButterflyEggItem(Butterfly.GLASSWING_NAME, new Item.Properties()));

    public static final RegistryObject<Item> HAIRSTREAK_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.HAIRSTREAK_NAME,
            () -> new ButterflyEggItem(Butterfly.HAIRSTREAK_NAME, new Item.Properties()));

    public static final RegistryObject<Item> HEATH_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.HEATH_NAME,
            () -> new ButterflyEggItem(Butterfly.HEATH_NAME, new Item.Properties()));

    public static final RegistryObject<Item> LONGWING_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.LONGWING_NAME,
            () -> new ButterflyEggItem(Butterfly.LONGWING_NAME, new Item.Properties()));

    public static final RegistryObject<Item> MONARCH_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.MONARCH_NAME,
            () -> new ButterflyEggItem(Butterfly.MONARCH_NAME, new Item.Properties()));

    public static final RegistryObject<Item> MORPHO_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.MORPHO_NAME,
            () -> new ButterflyEggItem(Butterfly.MORPHO_NAME, new Item.Properties()));

    public static final RegistryObject<Item> RAINBOW_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.RAINBOW_NAME,
            () -> new ButterflyEggItem(Butterfly.RAINBOW_NAME, new Item.Properties()));

    public static final RegistryObject<Item> SWALLOWTAIL_BUTTERFLY_EGG = INSTANCE.register(ButterflyEggItem.SWALLOWTAIL_NAME,
            () -> new ButterflyEggItem(Butterfly.SWALLOWTAIL_NAME, new Item.Properties()));

Butterfly Leaves Block

We want to be able to plant butterfly eggs on any leaves block in the game. Other than dropping the eggs when they are destroyed, we want them to behave in exactly the same way as the vanilla leaves blocks. The easiest way to do this is to inherit our classes from the default leaves blocks, but this poses a problem. They don’t inherit from BaseEntityBlock, and thus we can’t store an Entity ID using a Block Entity.

Our solution is to instead use the Block State to store our butterfly type. Unfortunately, block states can’t hold strings so we need to represent them using either an enumeration or an integer. We create a class to help us convert between Entity IDs and integers or vice-versa.

/**
 * Helper for converting entity IDs to indexes and vice versa.
 */
public class ButterflyIds {

    //  Helper maps.
    private static final Map<String, Integer> ENTITY_ID_TO_INDEX_MAP = new HashMap<>();
    private static final Map<Integer, String> INDEX_TO_ENTITY_ID_MAP = new HashMap<>();

    static {
        ENTITY_ID_TO_INDEX_MAP.put("admiral", 0);
        ENTITY_ID_TO_INDEX_MAP.put("buckeye", 1);
        ENTITY_ID_TO_INDEX_MAP.put("cabbage", 2);
        ENTITY_ID_TO_INDEX_MAP.put("chalkhill", 3);
        ENTITY_ID_TO_INDEX_MAP.put("clipper", 4);
        ENTITY_ID_TO_INDEX_MAP.put("common", 5);
        ENTITY_ID_TO_INDEX_MAP.put("emperor", 6);
        ENTITY_ID_TO_INDEX_MAP.put("forester", 7);
        ENTITY_ID_TO_INDEX_MAP.put("glasswing", 8);
        ENTITY_ID_TO_INDEX_MAP.put("hairstreak", 9);
        ENTITY_ID_TO_INDEX_MAP.put("heath", 10);
        ENTITY_ID_TO_INDEX_MAP.put("longwing", 11);
        ENTITY_ID_TO_INDEX_MAP.put("monarch", 12);
        ENTITY_ID_TO_INDEX_MAP.put("morpho", 13);
        ENTITY_ID_TO_INDEX_MAP.put("rainbow", 14);
        ENTITY_ID_TO_INDEX_MAP.put("swallowtail", 15);

        INDEX_TO_ENTITY_ID_MAP.put(0, "admiral");
        INDEX_TO_ENTITY_ID_MAP.put(1, "buckeye");
        INDEX_TO_ENTITY_ID_MAP.put(2, "cabbage");
        INDEX_TO_ENTITY_ID_MAP.put(3, "chalkhill");
        INDEX_TO_ENTITY_ID_MAP.put(4, "clipper");
        INDEX_TO_ENTITY_ID_MAP.put(5, "common");
        INDEX_TO_ENTITY_ID_MAP.put(6, "emperor");
        INDEX_TO_ENTITY_ID_MAP.put(7, "forester");
        INDEX_TO_ENTITY_ID_MAP.put(8, "glasswing");
        INDEX_TO_ENTITY_ID_MAP.put(9, "hairstreak");
        INDEX_TO_ENTITY_ID_MAP.put(10, "heath");
        INDEX_TO_ENTITY_ID_MAP.put(11, "longwing");
        INDEX_TO_ENTITY_ID_MAP.put(12, "monarch");
        INDEX_TO_ENTITY_ID_MAP.put(13, "morpho");
        INDEX_TO_ENTITY_ID_MAP.put(14, "rainbow");
        INDEX_TO_ENTITY_ID_MAP.put(15, "swallowtail");
    }

    /**
     * Converts an Entity ID to an index.
     * @param entityId The entity ID to convert.
     * @return The index of said entity ID.
     */
    public static int EntityIdToIndex(String entityId) {
        if (entityId.contains(":")) {
            String[] splits = entityId.split(":");
            entityId = splits[1];
        }

        if (ENTITY_ID_TO_INDEX_MAP.containsKey(entityId)) {
            return ENTITY_ID_TO_INDEX_MAP.get(entityId);
        }

        return -1;
    }

    /**
     * Convert an index to an entity ID.
     * @param index The index to convert.
     * @return The entity ID at the index.
     */
    public static String IndexToEntityId(int index) {
        if (INDEX_TO_ENTITY_ID_MAP.containsKey(index)) {
            return INDEX_TO_ENTITY_ID_MAP.get(index);
        }

        return null;
    }
}

With this we can create our simple ButterflyLeavesBlock implementation. We override createBlockStateDefinition to add our new property to the Block State.

/**
 * A class to represent leaves that have a butterfly egg inside them.
 */
public class ButterflyLeavesBlock extends LeavesBlock {

    // An integer representation of the butterfly species.
    public static final IntegerProperty BUTTERFLY_INDEX = IntegerProperty.create("butterfly_index", 0, 15);

    /**
     * Creates a new butterfly leaves block.
     * @param properties The block properties.
     */
    public ButterflyLeavesBlock(Properties properties) {
        super(properties);
        this.registerDefaultState(this.stateDefinition.any()
                .setValue(DISTANCE, 7)
                .setValue(PERSISTENT, Boolean.FALSE)
                .setValue(WATERLOGGED, Boolean.FALSE)
                .setValue(BUTTERFLY_INDEX, 0));
    }

    /**
     * Add the Butterfly Index to the block state definition.
     * @param builder The block state's builder.
     */
    @Override
    protected void createBlockStateDefinition(StateDefinition.@NotNull Builder<Block, BlockState> builder) {
        super.createBlockStateDefinition(builder);
        builder.add(BUTTERFLY_INDEX);
    }
}

To drop the eggs, we override getDrops() so we can add them to whatever drops the leaves may have had. In this way, whether they are destroyed or sheared, we will always get the butterfly egg.

    /**
     * Add the butterfly egg to any drops from the leaf.
     * @param blockState The block state being looted.
     * @param builder The loot builder.
     * @return The normal loot, plus the butterfly egg held within.
     */
    @NotNull
    @Override
    @SuppressWarnings("deprecation")
    public List<ItemStack> getDrops(@NotNull BlockState blockState,
                                    @NotNull LootParams.Builder builder) {

        List<ItemStack> result = super.getDrops(blockState, builder);

        int index = blockState.getValue(BUTTERFLY_INDEX);
        String entityId = ButterflyIds.IndexToEntityId(index);
        if (entityId != null) {
            Item entry = ForgeRegistries.ITEMS.getValue(new ResourceLocation(ButterfliesMod.MODID, entityId + "_egg"));
            if (entry != null) {
                result.add(new ItemStack(entry));
            }
        }

        return result;
    }

Since Cherry Trees and Mangrove Trees use their own classes, we also override those with similar blocks with these methods.

We can now register our leaves blocks in the Block Registry. Similar to how it is done for vanilla code, we create a helper function that can be used for most leaf blocks, only using custom properties for Cherry Leaves and Mangrove Leaves.

// Represent leaves that have butterfly eggs in them.
    public static final RegistryObject<Block> BUTTERFLY_OAK_LEAVES =
            INSTANCE.register("butterfly_oak_leaves", () -> butterflyLeaves(SoundType.GRASS));

    public static final RegistryObject<Block> BUTTERFLY_SPRUCE_LEAVES =
            INSTANCE.register("butterfly_spruce_leaves", () -> butterflyLeaves(SoundType.GRASS));

    public static final RegistryObject<Block> BUTTERFLY_BIRCH_LEAVES =
            INSTANCE.register("butterfly_birch_leaves", () -> butterflyLeaves(SoundType.GRASS));

    public static final RegistryObject<Block> BUTTERFLY_JUNGLE_LEAVES =
            INSTANCE.register("butterfly_jungle_leaves", () -> butterflyLeaves(SoundType.GRASS));

    public static final RegistryObject<Block> BUTTERFLY_ACACIA_LEAVES =
            INSTANCE.register("butterfly_acacia_leaves", () -> butterflyLeaves(SoundType.GRASS));

    public static final RegistryObject<Block> BUTTERFLY_DARK_OAK_LEAVES =
            INSTANCE.register("butterfly_dark_oak_leaves", () -> butterflyLeaves(SoundType.GRASS));

    public static final RegistryObject<Block> BUTTERFLY_AZALEA_LEAVES =
            INSTANCE.register("butterfly_azalea_leaves", () -> butterflyLeaves(SoundType.AZALEA_LEAVES));

    public static final RegistryObject<Block> BUTTERFLY_FLOWERING_AZALEA_LEAVES =
            INSTANCE.register("butterfly_flowering_azalea_leaves", () -> butterflyLeaves(SoundType.AZALEA_LEAVES));

    public static final RegistryObject<Block> BUTTERFLY_CHERRY_LEAVES =
            INSTANCE.register("butterfly_cherry_leaves", () -> new ButterflyCherryLeavesBlock(BlockBehaviour.Properties.of()
                    .mapColor(MapColor.COLOR_PINK)
                    .strength(0.2F)
                    .randomTicks()
                    .sound(SoundType.CHERRY_LEAVES)
                    .noOcclusion()
                    .isValidSpawn(BlockRegistry::ocelotOrParrot)
                    .isSuffocating(BlockRegistry::never)
                    .isViewBlocking(BlockRegistry::never)
                    .ignitedByLava()
                    .pushReaction(PushReaction.DESTROY)
                    .isRedstoneConductor(BlockRegistry::never)));

    public static final RegistryObject<Block> BUTTERFLY_MANGROVE_LEAVES =
            INSTANCE.register("butterfly_mangrove_leaves", () -> new ButterflyMangroveLeavesBlock(BlockBehaviour.Properties.of()
                    .mapColor(MapColor.PLANT)
                    .strength(0.2F)
                    .randomTicks()
                    .sound(SoundType.GRASS)
                    .noOcclusion()
                    .isValidSpawn(BlockRegistry::ocelotOrParrot)
                    .isSuffocating(BlockRegistry::never)
                    .isViewBlocking(BlockRegistry::never)
                    .ignitedByLava()
                    .pushReaction(PushReaction.DESTROY)
                    .isRedstoneConductor(BlockRegistry::never)));

    private static ButterflyLeavesBlock butterflyLeaves(SoundType p_152615_) {
        return new ButterflyLeavesBlock(BlockBehaviour.Properties.of()
                .mapColor(MapColor.PLANT)
                .strength(0.2F)
                .randomTicks()
                .sound(p_152615_)
                .noOcclusion()
                .isValidSpawn(BlockRegistry::ocelotOrParrot)
                .isSuffocating(BlockRegistry::never)
                .isViewBlocking(BlockRegistry::never)
                .ignitedByLava()
                .pushReaction(PushReaction.DESTROY)
                .isRedstoneConductor(BlockRegistry::never));
    }

The next thing to do here is to add the plantButterflyEgg() method. This is a static method that checks to see if the block is a leaf block, and if it is, converts it to a butterfly leaf block. When we do so, we make sure to copy the block state from the original leaf block.

In early tests I hadn’t done this, and this meant that when butterflies laid eggs, the blocks always acted as if they had spawned naturally. This means that if a player uses leaf blocks in their builds, the new blocks would decay if there was no wood near them, and had the potential to completely destroy builds.

    /**
     * Plants an egg on the specified block it it is a leaf block.
     * @param level The current level.
     * @param position The position of the block.
     * @param entityId The entity ID of the butterfly.
     * @return True if the egg was successfully planted, otherwise false.
     */
    public static boolean plantButterflyEgg(Level level,
                                            BlockPos position,
                                            String entityId) {
        BlockState blockState = level.getBlockState(position);

        if (blockState.getBlock() == Blocks.OAK_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_OAK_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.SPRUCE_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_SPRUCE_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.BIRCH_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_BIRCH_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.JUNGLE_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_JUNGLE_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.ACACIA_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_ACACIA_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.DARK_OAK_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_DARK_OAK_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.AZALEA_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_AZALEA_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.FLOWERING_AZALEA_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_FLOWERING_AZALEA_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.CHERRY_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_CHERRY_LEAVES);
            return true;
        }

        if (blockState.getBlock() == Blocks.MANGROVE_LEAVES) {
            plantButterflyEgg(level, position, entityId, BlockRegistry.BUTTERFLY_MANGROVE_LEAVES);
            return true;
        }

        return false;
    }

    /**
     * Plants an egg in the leaf block.
     * @param level The current level.
     * @param position The position of the block.
     * @param block The block to replace the current one with.
     */
    private static void plantButterflyEgg(Level level,
                                          BlockPos position,
                                          String entityId,
                                          RegistryObject<Block> block) {
        BlockState oldBlockState = level.getBlockState(position);

        // Create a new block and copy the old state.
        BlockState newBlockState = block.get().defaultBlockState();
        newBlockState = newBlockState
                .setValue(LeavesBlock.DISTANCE, oldBlockState.getValue(LeavesBlock.DISTANCE))
                .setValue(LeavesBlock.PERSISTENT, oldBlockState.getValue(LeavesBlock.PERSISTENT))
                .setValue(LeavesBlock.WATERLOGGED, oldBlockState.getValue(LeavesBlock.WATERLOGGED));

        // Try and get the species index and save this state as well.
        int index = ButterflyIds.EntityIdToIndex(entityId);
        if (index >= 0) {
            newBlockState = newBlockState.setValue(ButterflyLeavesBlock.BUTTERFLY_INDEX, index);
        }

        // Update the block to the new block.
        level.setBlockAndUpdate(position, newBlockState);
    }

    /**
     * Creates a new butterfly leaves block.
     * @param properties The block properties.
     */
    public ButterflyLeavesBlock(Properties properties) {
        super(properties);
        this.registerDefaultState(this.stateDefinition.any()
                .setValue(DISTANCE, 7)
                .setValue(PERSISTENT, Boolean.FALSE)
                .setValue(WATERLOGGED, Boolean.FALSE)
                .setValue(BUTTERFLY_INDEX, 0));
    }

Finally, the blocks need models and loot tables. Since we want them to look and behave as normal leaf blocks, all we need to do is copy the block states and loot tables for the vanilla leaves and rename them.

Block Colours

As anyone knows, in Minecraft foliage, such as leaves, changes colour depending on the biome it’s placed in.

Without registering colours, the new blocks appear grey.

This is implemented using a separate registry for block colours. By registering a functor to a specific block, the colour can be set based on certain conditions. We can do this by listening for the RegisterColorHandlersEvent.Block on the MOD event bus.

    /**
     * Register colors for (e.g.) foliage-style blocks.
     * @param event The event fired to let us know we can register block colors.
     */
    @SubscribeEvent
    public static void registerBlockColors(RegisterColorHandlersEvent.Block event){
        // Evergreen
        event.register((state, tint, position, color) -> FoliageColor.getEvergreenColor(),
                BUTTERFLY_SPRUCE_LEAVES.get());

        // Birch
        event.register((state, tint, position, color) -> FoliageColor.getBirchColor(),
                BUTTERFLY_BIRCH_LEAVES.get());

        // Default
        event.register((state, tint, position, color) ->
                        tint != null && position != null ? BiomeColors.getAverageFoliageColor(tint, position) :
                                                           FoliageColor.getDefaultColor(),
                BUTTERFLY_OAK_LEAVES.get(),
                BUTTERFLY_JUNGLE_LEAVES.get(),
                BUTTERFLY_ACACIA_LEAVES.get(),
                BUTTERFLY_DARK_OAK_LEAVES.get(),
                BUTTERFLY_MANGROVE_LEAVES.get());
    }

We don’t need to create our own methods here – we can just reuse the vanilla methods in FoliageColor so they look the same as the leaves they are based upon.

Laying Eggs

The final piece of this feature is to have butterflies actually lay eggs occasionally. We are keeping the behaviour simple here: essentially if a butterfly moves near a leaf block there is a chance that it will lay an egg. We do this by adding some simple behaviour to our customServerAiStep().

    protected void customServerAiStep() {
        super.customServerAiStep();

        // <snip>

        // Attempt to lay an egg.
        if (this.random.nextInt(20) == 1) {
            BlockPos position = this.blockPosition();
            position = switch (this.random.nextInt(6)) {
                default -> position.above();
                case 1 -> position.below();
                case 2 -> position.north();
                case 3 -> position.east();
                case 4 -> position.south();
                case 5 -> position.west();
            };

            ButterflyLeavesBlock.plantButterflyEgg(level, position, this.getEncodeId());
        }
    }

We could use a much more complex algorithm to create something that behaves in a more realistic way, however it’s often possible to give the appearance of complex behaviour using simple rules.

Feature Complete?

We now have butterfly eggs in the mod! We still need to complete the life cycle before we are finished, however. Next we will have the eggs hatch into caterpillars. The caterpillars will go on to form chrysalises, and finally spawn butterflies to complete the life cycle.

Until then, we have eggs to collect!

2 thoughts on “Laying Butterfly Eggs in Leaves

Comments are closed.