There was a butterfly I spotted in the Buddha Park in Vientiane that I somehow forgot about. It was an old Peacock Pansy Butterfly, its wings shredded and worn. So this week I worked on adding this butterfly to the mod, as well as yet another unique feature for it.
While writing the article about Vientiane’s Buddha Park the other day, I found a picture of a butterfly I had completely forgotten about. I figured out that it was a Peacock Pansy, though it looked old with its tattered wings.

Naturally, I decided to add the butterfly to Bok’s Banging Butterflies. While doing research on this butterfly, I found that it has seasonal polymorphism, meaning that it looks different depending on what season it emerges from its chrysalis. I thought this would also be a cool feature to add to the mod.
Variants
But how to achieve this? I settled on a system that would use variants for each butterfly. The variants would be entirely optional. Any butterfly that didn’t have variants (i.e. most of them), would just ignore this system.
We’ve actually already seen a variant already, with dimorphic butterflies that have male and female variants. I’ve adapted their implementation to take advantage of this new system. This is how to define a female moth that uses the male variant as a mate:
"variants": { "base": "spongy", "mate": "spongymale" }
We can use the same JSON to define the warm and cold variants of a butterfly as well:
"variants": { "base": "peacock-pansy-dry", "cold": "peacock-pansy-dry", "warm": "peacock-pansy-wet" }
To read in this data we need to modify the serializer in our ButterflyData
class. As you can see it’s written defensively so that it won’t break if a butterfly has no variants:
/** * Class to help serialize a butterfly entry. */ public static class Serializer implements JsonDeserializer<ButterflyData> { /** * Deserializes a JSON object into a butterfly entry * @param json The Json data being deserialized * @param typeOfT The type of the Object to deserialize to * @param context Language context (ignored) * @return A new butterfly entry * @throws IllegalArgumentException Unused */ @Override public ButterflyData deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws IllegalArgumentException { ButterflyData entry = null; if (json instanceof final JsonObject object) { // <snip> This is where the code for all the other values would be. String baseVariant = entityId; String coldVariant = entityId; String mateVariant = entityId; String warmVariant = entityId; JsonElement variantElement = object.get("variants"); if (variantElement != null) { JsonObject variants = variantElement.getAsJsonObject(); JsonElement baseElement = variants.get("base"); if (baseElement != null) { baseVariant = baseElement.getAsString(); } JsonElement coldElement = variants.get("cold"); if (coldElement != null) { coldVariant = coldElement.getAsString(); } JsonElement mateElement = variants.get("mate"); if (mateElement != null) { mateVariant = mateElement.getAsString(); } JsonElement warmElement = variants.get("warm"); if (warmElement != null) { warmVariant = warmElement.getAsString(); } } entry = new ButterflyData( index, entityId, size, speed, rarity, habitats, LIFESPAN[eggLifespan.getIndex()], LIFESPAN[caterpillarLifespan.getIndex()], LIFESPAN[chrysalisLifespan.getIndex()], LIFESPAN[butterflyLifespan.getIndex()], new ResourceLocation(preferredFlower), type, diurnality, extraLandingBlocks, plantEffect, eggMultiplier, caterpillarSounds, butterflySounds, traits, baseVariant, coldVariant, mateVariant, warmVariant ); } return entry; }
With the new system in place, I removed the old breedTarget
system and updated the network code to sync the new variant data instead.
Finally, I create some accessors to get butterfly indices for these values. Only the baseVariant
falls back on the default butterfly index. In this way, we can tell from outside these methods if the variant exists.
/** * Returns the butterfly index of the butterfly's base variant. * @return The index of the butterfly to try and mate with. */ public int getBaseButterflyIndex() { int index = getButterflyIndex(this.baseVariant); if (index < 0) { index = this.butterflyIndex; } return index; } /** * Returns the butterfly index of the butterfly's cold variant. * @return The index of the butterfly to try and mate with. */ public int getColdButterflyIndex() { return getButterflyIndex(this.coldVariant); } /** * Returns the butterfly index of the butterfly's mate. * @return The index of the butterfly to try and mate with. */ public int getMateButterflyIndex() { return getButterflyIndex(this.mateVariant); } /** * Returns the butterfly index of the butterfly's warm variant. * @return The index of the butterfly to try and mate with. */ public int getWarmButterflyIndex() { return getButterflyIndex(this.warmVariant); }
Implementing Variants
Now that we’ve defined the variants, here’s how they’re used in-game. The first place to look is in the customServerAiStep()
method on the Chrysalis
entity, which is where we spawn a new butterfly:
/** * A custom step for the AI update loop. */ @Override protected void customServerAiStep() { super.customServerAiStep(); // If the surface block is destroyed then the chrysalis dies. if (this.level().isEmptyBlock(getSurfaceBlockPos())) { kill(); } // Spawn Butterfly. if (this.getAge() >= 0 && this.random.nextInt(0, 15) == 0) { // The base, fallback variant. int butterflyIndex = getData().getBaseButterflyIndex(); // Check for cold and warm variants. float temperature = this.level().getBiome(this.blockPosition()).get().getModifiedClimateSettings().temperature(); if (temperature < 0.5) { int coldIndex = getData().getColdButterflyIndex(); if (coldIndex >= 0) { butterflyIndex = coldIndex; } } else { int warmIndex = getData().getWarmButterflyIndex(); if (warmIndex >= 0) { butterflyIndex = warmIndex; } } // Check for dimorphic variants. if (random.nextInt() % 2 == 0) { int mateIndex = getData().getMateButterflyIndex(); if (mateIndex >= 0) { butterflyIndex = mateIndex; } } ButterflyData data = ButterflyData.getEntry(butterflyIndex); if (data != null) { ResourceLocation newLocation = data.getButterflyEntity(); Butterfly.spawn(this.level(), newLocation, this.blockPosition(), false); this.remove(RemovalReason.DISCARDED); } } }
This code first gets the butterfly index to use before getting the ResourceLocation
for the entity. Depending on the temperature, either a cold or warm variant will spawn. It also decides 50/50 if a male or female variant will spawn. This new code replaces the way breedTarget
was used previously, but achieves the same result.
The other thing that needs changing is the creation of an egg. With male/female variants, the male couldn’t lay eggs so this was never an issue. This time, however, both variants can lay eggs, and we want them to both lay the same kind of egg.
This is where the base variant comes in, and we use this in the ButterflyLayEggGoal
to ensure all variants use the base variant’s type when an egg is spawned:
/** * Update the butterfly after it has landed. */ @Override public void tick() { super.tick(); if (this.butterfly.getIsLanded()) { this.tryTicks -= 11; if (this.butterfly.getIsFertile()) { // <snip> Some checks to ensure we don't spawn too many butterflies { Direction direction = this.butterfly.getLandedDirection().getOpposite(); if (this.butterfly.level().getBlockState(this.blockPos.relative(direction)).isAir()) { // Always use the base butterfly type for eggs. ButterflyData data = ButterflyData.getEntry(this.butterfly.getData().getBaseButterflyIndex()); if (data != null) { ResourceLocation eggEntity = data.getButterflyEggEntity(); ButterflyEgg.spawn((ServerLevel) this.butterfly.level(), eggEntity, this.blockPos, direction); this.butterfly.setIsFertile(false); this.butterfly.useEgg(); } } } } } }
Now all butterflies will spawn their “base variant’s” eggs, so as long as the data is set up right there will only be one kind of egg, caterpillar, and chrysalis for all variants.
All Variants are “Male”
To allow achievements and progression paths to be generated properly, all variant butterflies are categorized under the existing male data folder. This doesn’t imply they’re all male in-game, it’s just a practical simplification for data generation. I plan to rename this folder later to reflect its broader role.
4.5.0
As is usual, at the time of writing a release has already been made. So you can update the mod in your modpacks and start trying to find these new butterflies, as well as experimenting with these new variants. Both variants will spawn in plains, but you’ll also find the dry variants in savannas, and the wet variants in wetlands.

The Peacock Pansy is a small addition on paper, but it represents a growing depth in the mod’s design where even subtle ecological behaviours are starting to shape gameplay. I’m excited to keep building on this foundation and exploring what other features real-world butterflies can inspire.