I’ve spent this week adding as many moths to the mod as I can. Each new moth led to a few little changes to either fix bugs I found along the way, or to add a new behaviour for the moth. Here’s a list of what I’ve added and the changes I made for each one.
Domestic Silk Moth
I like the wings on this moth – I managed to achieve a different shape to the other butterflies and moths in the mod. Domestic silk moths are small, white and fluffy, as are their larvae, so I tried to reflect this in the design.
This is the first moth that is crepuscular (active during twilight). It is also the moth that you can farm silk from. Since in real life you have to boil the moth’s cocoon and kill the moth to get the silk (I actually didn’t know this), I’ve made it a drop that you get after killing the cocoon. I didn’t need to code anything new for this, I just created a loot table.
{ "type": "minecraft:entity", "pools": [ { "bonus_rolls": 0.0, "entries": [ { "type": "minecraft:item", "functions": [ { "add": false, "count": { "type": "minecraft:uniform", "max": 3.0, "min": 1.0 }, "function": "minecraft:set_count" }, { "count": { "type": "minecraft:uniform", "max": 1.0, "min": 0.0 }, "function": "minecraft:looting_enchant" } ], "name": "butterflies:silk" } ], "rolls": 1.0 } ], "random_sequence": "butterflies:entities/domestic_silk_chrysalis" }
This will drop between 1 and 3 silk when a cocoon is killed (or 1-4 if the weapon has Looting).
After I went through and created the domestic silk moth, I realised I had messed up. Since the ID I used for the moth, domestic_silk
has an underscore in it, it screwed up a lot of the code that extracts the species from the ID. Rather than redo all the work, I fixed it with a hack, but made a note to never use an underscore in a butterfly ID again.
// Kind of hacky. We should avoid butterfly IDs with // underscores in the future. Making an exception here, so we // don't lose work done before we realised it was a problem. if (species.contains("domestic_silk")) { return "domestic_silk"; }
I did have a bit of luck with this, since it highlighted an issue with the Butterfly Egg class. It hadn’t been updated to use a getData()
method like I had implemented in the Butterfly class, so there was a chance that the data could be accessed before it existed. I implemented the same solution to the butterfly egg, which should make the code more robust in the future.
Peppered Moth
The peppered moth was hard to design. I wanted to get that mottled camouflage look without it looking too much like random noise. I made this mistake with the original buckeye butterfly, which is why I ended up redesigning it. The trick I found for this moth was to use a pattern rather than random noise, and to use transparency to blend the darker parts with the light. I quite like what I ended up with.
Peppered moths can land on logs as well as leaves, so I needed a way to support that. I achieved this using the extraLandingBlocks
code I wrote for clothes moths, and a slight modification to the isValidLandingBlock()
method.
/** * Check if the current block is a valid landing block. * @param blockState The block state to check. * @return TRUE if the butterfly can land on the block. */ public boolean isValidLandingBlock(BlockState blockState) { if (blockState.is(BlockTags.LEAVES)) { return true; } // Handle extra block types return switch (extraLandingBlocks) { case WOOL -> blockState.is(BlockTags.WOOL); case LOGS -> blockState.is(BlockTags.LOGS); default -> false; }; }
Indian Meal Moth
The Indian Meal Moth has an interesting look. The top half is brown and the lower half is white. I tried to reflect that in the design of this particular moth. It was a little tricky making sure the two colors blended together and it didn’t look like a cut and shut.
This moth lands on hay bales, so I added them as an option to the extraLandingBlocks
, just like I did with logs. These moths will also eat grain, and I reflected this by creating a goal that allows them to consume wheat. The goal will reduce the AGE
of a crop by 1, but will never destroy a crop entirely. It’s heavily based on the pollinate goal, using the preferredFlower
attribute to determine what crop to eat.
/** * Goal that enables butterflies to eat crops. */ public class ButterflyEatCropGoal extends MoveToBlockGoal { // The butterfly using this goal. private final Butterfly butterfly; // The flower this butterfly prefers. private CropBlock foodSource = null; // The RNG. private final RandomSource random; // Has pollination been attempted yet? private boolean hasEaten; /** * Construction * @param mob The instance of the butterfly. * @param speedModifier The speed modifier applied when this goal is in progress. * @param searchRange The range to search for blocks. * @param verticalSearchRange The vertical range to search for blocks. */ @SuppressWarnings("deprecation") public ButterflyEatCropGoal(Butterfly mob, double speedModifier, int searchRange, int verticalSearchRange) { super(mob, speedModifier, searchRange, verticalSearchRange); this.butterfly = mob; ButterflyData data = ButterflyData.getEntry(this.butterfly.getButterflyIndex()); if (data != null) { Block potentialFoodSource = BuiltInRegistries.BLOCK.get(data.preferredFlower()); if (potentialFoodSource instanceof CropBlock cropBlock) { this.foodSource = cropBlock; } } this.random = this.butterfly.getRandom(); } /** * Increase the accepted distance. * @return A distance of 2 blocks. */ @Override public double acceptedDistance() { return 2.0; } /** * Stop using if time of day changes to inactive. * @return Whether the goal can continue being active. */ @Override public boolean canContinueToUse() { return this.butterfly.getIsActive() && super.canContinueToUse(); } /** * Butterflies can only pollinate when active. * @return TRUE if the butterfly can pollinate right now. */ @Override public boolean canUse() { return this.butterfly.getIsActive() && super.canUse(); } /** * Start using the goal - ensure the butterfly is not landed. */ @Override public void start() { hasEaten = false; super.start(); } /** * Update the butterfly after it has landed. */ @Override public void tick() { super.tick(); if (this.isReachedTarget()) { // Don't stay in the landed state for too long. this.tryTicks -= 11; Vec3 deltaMovement = this.butterfly.getDeltaMovement(); this.butterfly.setDeltaMovement(0.0, deltaMovement.y, 0.0); if (!hasEaten) { hasEaten = true; BlockState blockState = this.mob.level().getBlockState(this.blockPos); if (blockState.is(this.foodSource)) { int age = blockState.getValue(CropBlock.AGE); if (age > 0) { blockState.setValue(CropBlock.AGE, age -1); this.mob.level().setBlockAndUpdate(this.blockPos, blockState); } } } } } /** * Tells the base goal which blocks are valid targets. * @param levelReader Gives access to the level. * @param blockPos The block position to check. * @return TRUE if the block is a valid target. */ @Override protected boolean isValidTarget(@NotNull LevelReader levelReader, @NotNull BlockPos blockPos) { if (!levelReader.getBlockState(blockPos.above()).isAir()) { return false; } BlockState blockState = levelReader.getBlockState(blockPos); return blockState.is(this.foodSource); } }
To support this goal, I added a new attribute to the butterflies, the PlantEffect
.
// The effect the butterfly has on plants. public enum PlantEffect { NONE, POLLINATE, CONSUME }
Using this, I can now update the Butterfly’s addGoal()
method to select a goal to add.
// Pollination can be configured to be off. if (ButterfliesConfig.enablePollination.get()) { switch (this.getData().plantEffect()) { case NONE: break; case POLLINATE: this.goalSelector.addGoal(4, new ButterflyPollinateFlowerGoal(this, 0.8d, 8, 8)); break; case CONSUME: this.goalSelector.addGoal(4, new ButterflyEatCropGoal(this, 0.8d, 8, 8)); break; } }
NONE
isn’t used at the moment, but it gives the option to create moths or butterflies that ignore plants completely. Maybe I’ll make use of that option someday.
Spongy Moth
This moth is a bit of an experiment. The males and females of this species look very different, so I wanted to represent this dimorphism using two separate butterflies. It was tricky to figure out how to code it, but I managed to create a moth species that has a different appearance depending on its biological sex.
To achieve this, I actually created two different species, and used a couple of extra attributes to change their behaviour. The breedTarget
attribute will tell the game which butterfly they should target when they want to mate, and the numEggs
attribute sets the number of eggs to either NONE
, NORMAL
, or DOUBLE
. In this way I could just set the female moth to have double the eggs, and the male moth to have zero eggs, then set their breed target to each other.
To support this, I changed how butterfly mating works slightly. I added a method to the ButterflyData
that would get the butterfly index of the mate. This will fall back to the same index as before if the mate is invalid.
/** * Returns the butterfly index of the butterfly's mate. * @return The index of the butterfly to try and mate with. */ public int getMateButterflyIndex() { int mateIndex = getButterflyIndex(this.breedTarget); if (mateIndex < 0) { mateIndex = this.butterflyIndex; } return mateIndex; }
Next I changed the target selector to check for this index instead.
// Butterflies use targets to select mates. this.targetSelector.addGoal(0, new NearestAttackableTargetGoal<>(this, Butterfly.class, true, (target) -> { if (target instanceof Butterfly butterfly) { return butterfly.getButterflyIndex() == this.getData().getMateButterflyIndex() && butterfly.getNumEggs() > 0 && !butterfly.getIsFertile(); } return false; }));
So now we can have an entity attempt to mate with a different entity, but we also need to modify the goal slightly. Currently the goal will fertilise the eggs of the butterfly using the goal. I altered it so that it fertilise the butterfly that is targeted by the goal. In this way the male (with no eggs) will target and fertilise the female’s eggs (since they have them).
/** * Can only use the goal if butterflies have eggs that need fertilising. * @return TRUE if the butterfly can mate. */ @Override public boolean canUse() { return this.butterfly.getIsActive() && super.canUse(); } /** * Do the actual mating if the butterflies get close enough to each other. */ @Override public void tick() { super.tick(); LivingEntity target = this.butterfly.getTarget(); if (target instanceof Butterfly mate) { if (!mate.getIsFertile()) { if (this.butterfly.distanceToSqr(target) < MATING_DISTANCE_SQUARED) { mate.setIsFertile(true); mate.setInLove(null); } } } }
The last change to mating is setting the number of eggs for each entity. With a small change in the constructor, the eggs will be modified based on the butterfly data.
// Small chance the butterfly has more eggs. int numEggs = ButterfliesConfig.eggLimit.get(); if (this.random.nextDouble() < ButterfliesConfig.doubleEggChance.get()) { numEggs *= 2; } switch (getData().eggMultiplier()) { case NONE -> numEggs = 0; case NORMAL -> { } case DOUBLE -> numEggs *= 2; } setNumEggs(numEggs);
Now we need to make sure that both sexes can spawn when a chrysalis matures. To do this I added a new method to the ButterflyData
that works similar to getMateButterflyIndex()
.
/** * Gets the entity that a chrysalis will spawn. For dimorphic species there * is a 50/50 chance it will be male or female. * @param random A random source used to determine the sex of dimorphic species. * @return The entity ID of the butterfly that should spawn. */ public ResourceLocation getMateButterflyEntity(RandomSource random) { int mateIndex = getMateButterflyIndex(); if (random.nextInt() % 2 == 0) { ButterflyData entry = getEntry(mateIndex); if (entry != null) { return entry.getButterflyEntity(); } } return this.getButterflyEntity(); }
Now all we need to do is update the code in the Chrysalis
class to use this method instead of the old one.
/** * 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) { ResourceLocation newLocation = getData().getMateButterflyEntity(this.random); Butterfly.spawn(this.level(), newLocation, this.blockPosition(), false); this.remove(RemovalReason.DISCARDED); } }
And that’s it for the code. Now I can set up their butterfly data. The female butterfly will have double the eggs and be larger than the male.
{ "breedTarget": "spongymale", "diurnality": "crepuscular", "eggMultiplier": "double", "entityId": "spongy", "extraLandingBlocks": "none", "habitat": "forests", "index": 22, "lifespan": { "butterfly": "short", "caterpillar": "long", "chrysalis": "short", "egg": "long" }, "plantEffect": "consume", "preferredFlower": "bud_orange_tulip", "rarity": "common", "size": "medium", "speed": "moderate", "type": "moth" }
Whereas the male butterfly will have no eggs and be smaller. It will be more active than the female moth, though.
{ "breedTarget": "spongy", "diurnality": "nocturnal", "eggMultiplier": "none", "entityId": "spongymale", "extraLandingBlocks": "none", "habitat": "forests", "index": 23, "lifespan": { "butterfly": "long", "caterpillar": "short", "chrysalis": "short", "egg": "long" }, "plantEffect": "consume", "preferredFlower": "bud_orange_tulip", "rarity": "common", "size": "small", "speed": "moderate", "type": "moth" }
So now I have the first ever dimorphic species in the mod. I’m not sure if I’ll add more, but with the way it’s been implemented it should be easier to add in the future. First I need to test this new species rigorously to make sure I haven’t broken anything…
More Moths and Better Butterflies
I’m planning to add at least four more moths to the mod. After that I want to go back and add some unique behaviours to the butterflies that are already in the mod. I’ve also been spotting many butterflies as I travel through Laos right now, so I’m planning to add a few more based on the butterflies I’ve spotted so far. I saw a Common Birdwing in Vientiane recently, and not only is this butterfly the biggest one I’ve ever seen, it’s absolutely beautiful.
With the next release, there will be moths flying around at night, more butterflies during the day, and they will each have unique behaviour that will make discovering and playing around with them just a bit more interesting. It’s going to be the best version of the mod yet.