From c59b9366f6337ef19a2d5741ba6684e35fed0702 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Sun, 24 Mar 2024 22:08:11 +0000
Subject: [PATCH 01/14] Fix some advancement names and heirarchy

---
 .../datagen/providers/UAdvancementsProvider.java | 10 ++++++----
 .../resources/assets/unicopia/lang/en_us.json    | 16 ++++++++++------
 2 files changed, 16 insertions(+), 10 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
index b815f246..52c85a0c 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
@@ -64,7 +64,8 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
                 p.child(Items.CHIPPED_ANVIL).hidden().frame(AdvancementFrame.CHALLENGE).criterion("ding_sun", dingCelestia(Set.of(), Set.of(Race.BAT))).build(consumer, "blasphemy");
                 p.child(Items.CHIPPED_ANVIL).hidden().frame(AdvancementFrame.CHALLENGE).criterion("ding_sun", dingCelestia(Set.of(Race.BAT), Set.of())).build(consumer, "sweet_sweet_revenge");
             });
-            root.child(UItems.OATS).criterion("has_oats", hasItems(UItems.OATS)).build(consumer, "what_the_hay");
+            root.child(UItems.OATS).criterion("has_oats", hasItems(UItems.OATS)).build(consumer, "oats_so_easy");
+            root.child(Items.HAY_BLOCK).criterion("eat_hay", ConsumeItemCriterion.Conditions.item(Items.HAY_BLOCK)).build(consumer, "what_the_hay");
             root.child(UItems.IRON_HORSE_SHOE).criterion("killed_entity_with_horseshoe", killWithItems(UTags.FROM_HORSESHOES)).build(consumer, "dead_ringer");
             root.child(UItems.PINECONE).frame(AdvancementFrame.CHALLENGE).criterion("eat_pinecone", ConsumeItemCriterion.Conditions.item(UItems.PINECONE)).build(consumer, "eat_pinecone");
             root.child(UItems.GIANT_BALLOON).criterion("ride_balloon", CustomEventCriterion.create("ride_balloon")).build(consumer, "travelling_in_style");
@@ -152,7 +153,7 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
             p.child(UItems.CRYSTAL_SHARD).criterion("has_shard", hasItems(UItems.CRYSTAL_SHARD)).build(consumer, "crystaline").children(pp -> {
                pp.child(UItems.CRYSTAL_HEART).criterion("power_up_heart", CustomEventCriterion.create("power_up_heart")).rewards(AdvancementRewards.Builder.experience(105)).build(consumer, "power_up_heart");
             });
-            p.child(UItems.ALICORN_AMULET).criterion("has_alicorn_amulet", hasItems(UItems.ALICORN_AMULET)).build(consumer, "tempting")
+            p.child(UItems.ALICORN_AMULET).criterion("has_alicorn_amulet", hasItems(UItems.ALICORN_AMULET)).build(consumer, "tempted")
              .child(Items.CRYING_OBSIDIAN).criterion("light_altar", CustomEventCriterion.create("light_altar")).build(consumer, "hello_darkness_my_old_friend")
              .child(UItems.BROKEN_ALICORN_AMULET).frame(AdvancementFrame.GOAL).criterion("defeat_sombra", CustomEventCriterion.create("defeat_sombra")).rewards(AdvancementRewards.Builder.experience(2000)).build(consumer, "save_the_day")
              .children(pp -> {
@@ -177,7 +178,7 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
     }
 
     private void generateEnchantmentsAdvancementsTree(Consumer<Advancement> consumer) {
-        AdvancementDisplayBuilder.create(Items.NETHERITE_SCRAP)
+        AdvancementDisplayBuilder.create(Items.NETHERITE_SCRAP).showToast().announce()
             .criterion("enchant_with_consumption", enchant(UEnchantments.CONSUMPTION))
             .rewards(AdvancementRewards.Builder.experience(120))
             .parent(new Identifier("story/enchant_item"))
@@ -189,9 +190,10 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
                 .group("enchanting")
                 .hidden()
                 .build(consumer, "xp_miner");
-        AdvancementDisplayBuilder.create(Items.GOLDEN_APPLE)
+        AdvancementDisplayBuilder.create(Items.GOLDEN_APPLE).showToast().announce()
             .criterion("enchant_with_heart_bound", enchant(UEnchantments.HEART_BOUND))
             .rewards(AdvancementRewards.Builder.experience(120))
+            .parent(new Identifier("story/enchant_item"))
             .group("enchanting")
             .build(consumer, "hearts_stronger_than_horses")
             .child(Items.GOLDEN_PICKAXE)
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index ccba8c05..7599643d 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -1618,6 +1618,10 @@
   "advancements.unicopia.eat_trick_apple.description": "Bite into a zap apple",
   "advancements.unicopia.eat_pinecone.title": "Desperation",
   "advancements.unicopia.eat_pinecone.description": "Eat a pinecone",
+  "advancements.unicopia.what_the_hay.title": "What The Hay",
+  "advancements.unicopia.what_the_hay.description": "Eat an entire block of hay",
+  "advancements.unicopia.oats_so_easy.title": "Oats So Easy",
+  "advancements.unicopia.oats_so_easy.description": "Farm some oats",
   "advancements.unicopia.imported_oats.title": "Delicious As They Are Expensive",
   "advancements.unicopia.imported_oats.description": "Send or receive fancy imported oats",
 
@@ -1650,8 +1654,8 @@
   "advancements.unicopia.travelling_in_style.title": "Travelling in Style",
   "advancements.unicopia.travelling_in_style.description": "Ride a hot air balloon",
 
-  "advancements.unicopia.night_route.title": "Children of The Night",
-  "advancements.unicopia.night_route.description": "Walk the path of the night",
+  "advancements.unicopia.bat_route.title": "Children of The Night",
+  "advancements.unicopia.bat_route.description": "Walk the path of the night",
   "advancements.unicopia.screech_twenty_mobs.title": "Terror From The Skies",
   "advancements.unicopia.screech_twenty_mobs.description": "Rain down terror on at least 20 mobs at once",
   "advancements.unicopia.screech_self.title": "Jeepers!",
@@ -1675,8 +1679,8 @@
   "advancements.unicopia.thats_unusual.title": "That's Unusual",
   "advancements.unicopia.thats_unusual.description": "But what does it do?",
 
-  "advancements.unicopia.sky_route.title": "Path of the Pegasus",
-  "advancements.unicopia.sky_route.description": "Join the Clousdale Pegasi",
+  "advancements.unicopia.pegasus_route.title": "Path of the Pegasus",
+  "advancements.unicopia.pegasus_route.description": "Join the Clousdale Pegasi",
   "advancements.unicopia.molting_season_1.title": "Molting Season",
   "advancements.unicopia.molting_season_1.description": "Drop your first feather whilst flying",
   "advancements.unicopia.molting_season_2.title": "Molting Season 2",
@@ -1708,8 +1712,8 @@
   "advancements.unicopia.deter_phantom.title": "What Flies Around",
   "advancements.unicopia.deter_phantom.description": "Get up there and give those phantoms a taste of their own medicine",
 
-  "advancements.unicopia.magical_route.title": "Horn of the Unicorn",
-  "advancements.unicopia.magical_route.description": "Delve into the world of glitter and rainbows",
+  "advancements.unicopia.unicorn_route.title": "Horn of the Unicorn",
+  "advancements.unicopia.unicorn_route.description": "Delve into the world of glitter and rainbows",
   
   "advancements.unicopia.books.title": "Books!",
   "advancements.unicopia.books.description": "This is MY spellbook and I'm going to READ it!",

From ac5aedb4889c6968706f1f7eb22a5f16fa30cc78 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Sun, 24 Mar 2024 23:06:26 +0000
Subject: [PATCH 02/14] Fix some advancements and add some new ones

---
 .../unicopia/advancement/UCriteria.java       |  1 +
 .../providers/UAdvancementsProvider.java      | 55 ++++++++++---------
 .../unicopia/entity/mob/AirBalloonEntity.java |  6 ++
 .../resources/assets/unicopia/lang/en_us.json | 14 +++++
 4 files changed, 50 insertions(+), 26 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java b/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java
index db4d1224..339c0aac 100644
--- a/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java
+++ b/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java
@@ -28,6 +28,7 @@ public interface UCriteria {
     CustomEventCriterion.Trigger POWER_UP_HEART = CUSTOM_EVENT.createTrigger("power_up_heart");
     CustomEventCriterion.Trigger SPLIT_SEA = CUSTOM_EVENT.createTrigger("split_sea");
     CustomEventCriterion.Trigger RIDE_BALLOON = CUSTOM_EVENT.createTrigger("ride_balloon");
+    CustomEventCriterion.Trigger CONSTRUCT_BALLOON = CUSTOM_EVENT.createTrigger("construct_balloon");
     CustomEventCriterion.Trigger TELEPORT_ABOVE_WORLD = CUSTOM_EVENT.createTrigger("teleport_above_world");
 
     static void bootstrap() { }
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
index 52c85a0c..fc831f33 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
@@ -54,32 +54,37 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
             createTribeRootAdvancement(consumer, root, Race.PEGASUS).children(consumer, this::generatePegasusTribeAdvancementsTree);
             createTribeRootAdvancement(consumer, root, Race.UNICORN).children(consumer, this::generateUnicornTribeAdvancementsTree);
 
-            root.child(UItems.DRAGON_BREATH_SCROLL).criterion("has_scroll", hasItems(UItems.DRAGON_BREATH_SCROLL)).build(consumer, "take_a_note").children(p -> {
-                p.child(UItems.DRAGON_BREATH_SCROLL).criterion("send_scroll", dragonScroll(false, Items.WRITTEN_BOOK)).build(consumer, "dear_princess");
+            root.child(UItems.DRAGON_BREATH_SCROLL).showToast().announce().criterion("has_scroll", hasItems(UItems.DRAGON_BREATH_SCROLL)).build(consumer, "take_a_note").children(p -> {
+                p.child(UItems.DRAGON_BREATH_SCROLL).criterion("send_book", dragonScroll(false, Items.WRITTEN_BOOK)).build(consumer, "dear_princess")
+                 .child(UItems.DRAGON_BREATH_SCROLL).criterion("send_scroll", dragonScroll(false, UItems.DRAGON_BREATH_SCROLL)).build(consumer, "i_await_your_reply");
                 p.child(UItems.IMPORTED_OATS).hidden().frame(AdvancementFrame.CHALLENGE)
                     .criterion("send_oats", dragonScroll(false, UItems.OATS, UItems.IMPORTED_OATS))
-                    .criterion("receieve_oats", dragonScroll(true, UItems.OATS, UItems.IMPORTED_OATS))
-                    .criteriaMerger(CriterionMerger.OR)
-                    .build(consumer, "imported_oats");
+                    .criterion("receieve_oats", dragonScroll(true, UItems.IMPORTED_OATS))
+                    .criteriaMerger(CriterionMerger.OR).build(consumer, "imported_oats");
                 p.child(Items.CHIPPED_ANVIL).hidden().frame(AdvancementFrame.CHALLENGE).criterion("ding_sun", dingCelestia(Set.of(), Set.of(Race.BAT))).build(consumer, "blasphemy");
                 p.child(Items.CHIPPED_ANVIL).hidden().frame(AdvancementFrame.CHALLENGE).criterion("ding_sun", dingCelestia(Set.of(Race.BAT), Set.of())).build(consumer, "sweet_sweet_revenge");
             });
-            root.child(UItems.OATS).criterion("has_oats", hasItems(UItems.OATS)).build(consumer, "oats_so_easy");
-            root.child(Items.HAY_BLOCK).criterion("eat_hay", ConsumeItemCriterion.Conditions.item(Items.HAY_BLOCK)).build(consumer, "what_the_hay");
-            root.child(UItems.IRON_HORSE_SHOE).criterion("killed_entity_with_horseshoe", killWithItems(UTags.FROM_HORSESHOES)).build(consumer, "dead_ringer");
-            root.child(UItems.PINECONE).frame(AdvancementFrame.CHALLENGE).criterion("eat_pinecone", ConsumeItemCriterion.Conditions.item(UItems.PINECONE)).build(consumer, "eat_pinecone");
-            root.child(UItems.GIANT_BALLOON).criterion("ride_balloon", CustomEventCriterion.create("ride_balloon")).build(consumer, "travelling_in_style");
-            root.child(UItems.MUFFIN).hidden().criterion("has_muffin", hasItems(UItems.MUFFIN)).build(consumer, "baked_bads");
-            root.child(UItems.HORSE_SHOE_FRIES).criterion("has_horse_shoe_fries", hasItems(UItems.HORSE_SHOE_FRIES)).build(consumer, "lucky");
-            root.child(UItems.TOAST).criterion("has_toast", hasItems(UItems.TOAST)).build(consumer, "toast")
+            root.child(UItems.OATS).showToast().announce().criterion("has_oats", hasItems(UItems.OATS)).build(consumer, "oats_so_easy");
+            root.child(Items.HAY_BLOCK).showToast().announce().criterion("eat_hay", ConsumeItemCriterion.Conditions.item(Items.HAY_BLOCK)).build(consumer, "what_the_hay");
+            root.child(UItems.COPPER_HORSE_SHOE).showToast().announce().criterion("has_horseshoe", hasItems(UTags.HORSE_SHOES)).build(consumer, "blacksmith").children(p -> {
+                p.child(UItems.IRON_HORSE_SHOE).criterion("has_iron_horseshoe", hasItems(UItems.IRON_HORSE_SHOE)).build(consumer, "change_of_shoes")
+                 .child(UItems.GOLDEN_HORSE_SHOE).criterion("has_gold_horseshoe", hasItems(UItems.GOLDEN_HORSE_SHOE)).build(consumer, "fashionably_expensive")
+                 .child(UItems.NETHERITE_HORSE_SHOE).criterion("has_netherite_horseshoe", hasItems(UItems.NETHERITE_HORSE_SHOE)).build(consumer, "overkill");
+                p.child(UItems.IRON_HORSE_SHOE).hidden().frame(AdvancementFrame.CHALLENGE).criterion("killed_entity_with_horseshoe", killWithItems(UTags.FROM_HORSESHOES)).build(consumer, "dead_ringer");
+            });
+            root.child(UItems.PINECONE).showToast().announce().frame(AdvancementFrame.CHALLENGE).criterion("eat_pinecone", ConsumeItemCriterion.Conditions.item(UItems.PINECONE)).build(consumer, "eat_pinecone");
+            root.child(UItems.OAK_BASKET).showToast().criterion("has_basket", hasItems(UTags.BASKETS)).build(consumer, "basket_case")
+                .child(Items.LANTERN).showToast().criterion("construct_balloon", CustomEventCriterion.create("construct_balloon")).build(consumer, "aeronaut")
+                .child(UItems.GIANT_BALLOON).showToast().announce().frame(AdvancementFrame.CHALLENGE).criterion("ride_balloon", CustomEventCriterion.create("ride_balloon")).build(consumer, "travelling_in_style");
+            root.child(UItems.MUFFIN).showToast().announce().hidden().criterion("has_muffin", hasItems(UItems.MUFFIN)).build(consumer, "baked_bads");
+            root.child(UItems.HORSE_SHOE_FRIES).showToast().announce().criterion("has_horse_shoe_fries", hasItems(UItems.HORSE_SHOE_FRIES)).build(consumer, "lucky");
+            root.child(UItems.TOAST).showToast().announce().criterion("has_toast", hasItems(UItems.TOAST)).build(consumer, "toast")
                 .child(UItems.BURNED_TOAST).hidden().criterion("has_burned_toast", hasItems(UItems.BURNED_TOAST)).build(consumer, "burn_toast");
-            root.child(UItems.GREEN_APPLE).criterion("has_apple", hasItems(UTags.FRESH_APPLES)).build(consumer, "apple_route").children(p -> {
-                p.child(UItems.SWEET_APPLE)
-                    .criterion("has_all_apples", hasItems(Items.APPLE, UItems.GREEN_APPLE, UItems.SWEET_APPLE, UItems.SOUR_APPLE, UItems.ROTTEN_APPLE, UItems.ZAP_APPLE, UItems.COOKED_ZAP_APPLE, Items.GOLDEN_APPLE))
-                    .build(consumer, "sweet_apple_acres");
+            root.child(UItems.GREEN_APPLE).showToast().announce().criterion("has_apple", hasItems(UTags.FRESH_APPLES)).build(consumer, "apple_route").children(p -> {
+                p.child(UItems.SWEET_APPLE).criterion("has_all_apples", hasItems(Items.APPLE, UItems.GREEN_APPLE, UItems.SWEET_APPLE, UItems.SOUR_APPLE, UItems.ROTTEN_APPLE, UItems.ZAP_APPLE, UItems.COOKED_ZAP_APPLE, Items.GOLDEN_APPLE)).build(consumer, "sweet_apple_acres");
                 p.child(UItems.ZAP_BULB).criterion("has_zap_apple", hasItems(UItems.ZAP_APPLE)).build(consumer, "trick_apple").children(pp -> {
-                  pp.child(UItems.ZAP_APPLE).hidden().criterion("eat_trick_apple", CustomEventCriterion.createFlying("eat_trick_apple")).build(consumer, "eat_trick_apple");
-                  pp.child(UItems.ZAP_APPLE).hidden().criterion("feed_trick_apple", CustomEventCriterion.createFlying("feed_trick_apple")).build(consumer, "feed_trick_apple");
+                    pp.child(UItems.ZAP_APPLE).hidden().criterion("eat_trick_apple", CustomEventCriterion.createFlying("eat_trick_apple")).build(consumer, "eat_trick_apple");
+                    pp.child(UItems.ZAP_APPLE).hidden().criterion("feed_trick_apple", CustomEventCriterion.createFlying("feed_trick_apple")).build(consumer, "feed_trick_apple");
                 });
                 p.child(UItems.JUICE).criterion("has_juice", hasItems(UItems.JUICE)).build(consumer, "juice")
                  .child(UItems.BURNED_JUICE).hidden().criterion("has_burned_juice", hasItems(UItems.BURNED_JUICE)).build(consumer, "burn_juice")
@@ -151,12 +156,11 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
     private void generateUnicornTribeAdvancementsTree(Consumer<Advancement> consumer, AdvancementDisplayBuilder.Parent parent) {
         parent.child(UItems.SPELLBOOK).criterion("has_spellbook", hasItems(UItems.SPELLBOOK)).build(consumer, "books").children(p -> {
             p.child(UItems.CRYSTAL_SHARD).criterion("has_shard", hasItems(UItems.CRYSTAL_SHARD)).build(consumer, "crystaline").children(pp -> {
-               pp.child(UItems.CRYSTAL_HEART).criterion("power_up_heart", CustomEventCriterion.create("power_up_heart")).rewards(AdvancementRewards.Builder.experience(105)).build(consumer, "power_up_heart");
+                pp.child(UItems.CRYSTAL_HEART).criterion("power_up_heart", CustomEventCriterion.create("power_up_heart")).rewards(AdvancementRewards.Builder.experience(105)).build(consumer, "power_up_heart");
             });
             p.child(UItems.ALICORN_AMULET).criterion("has_alicorn_amulet", hasItems(UItems.ALICORN_AMULET)).build(consumer, "tempted")
              .child(Items.CRYING_OBSIDIAN).criterion("light_altar", CustomEventCriterion.create("light_altar")).build(consumer, "hello_darkness_my_old_friend")
-             .child(UItems.BROKEN_ALICORN_AMULET).frame(AdvancementFrame.GOAL).criterion("defeat_sombra", CustomEventCriterion.create("defeat_sombra")).rewards(AdvancementRewards.Builder.experience(2000)).build(consumer, "save_the_day")
-             .children(pp -> {
+             .child(UItems.BROKEN_ALICORN_AMULET).frame(AdvancementFrame.GOAL).criterion("defeat_sombra", CustomEventCriterion.create("defeat_sombra")).rewards(AdvancementRewards.Builder.experience(2000)).build(consumer, "save_the_day").children(pp -> {
                  pp.child(UItems.UNICORN_AMULET).frame(AdvancementFrame.GOAL).criterion("obtain_the_thing", hasItems(UItems.UNICORN_AMULET)).rewards(AdvancementRewards.Builder.experience(1100)).build(consumer, "ascension");
                  pp.child(UItems.BROKEN_ALICORN_AMULET).hidden().frame(AdvancementFrame.CHALLENGE).criterion("defeat_sombra_again", CustomEventCriterion.create("defeat_sombra", 2)).rewards(AdvancementRewards.Builder.experience(2000)).build(consumer, "doctor_sombrero");
              });
@@ -170,9 +174,8 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
     private void generateBatTribeAdvancementsTree(Consumer<Advancement> consumer, AdvancementDisplayBuilder.Parent parent) {
         parent.child(Items.LIGHT).criterion("look_into_sun", CustomEventCriterion.create("look_into_sun")).build(consumer, "praise_the_sun").children(p -> {
             p.child(UItems.SUNGLASSES).criterion("wear_shades", CustomEventCriterion.create("wear_shades")).build(consumer, "cool_potato");
-            p.child(Items.BLACK_CANDLE).frame(AdvancementFrame.CHALLENGE).criterion("screech_twenty_mobs", CustomEventCriterion.createFlying("screech_twenty_mobs")).build(consumer, "screech_twenty_mobs").children(pp -> {
-                pp.child(Items.BRICK).frame(AdvancementFrame.CHALLENGE).criterion("super_scare_entity", CustomEventCriterion.createFlying("super_scare_entity")).build(consumer, "extra_spooky");
-            });
+            p.child(Items.BLACK_CANDLE).frame(AdvancementFrame.CHALLENGE).criterion("screech_twenty_mobs", CustomEventCriterion.createFlying("screech_twenty_mobs")).build(consumer, "screech_twenty_mobs")
+             .child(Items.BRICK).frame(AdvancementFrame.CHALLENGE).criterion("super_scare_entity", CustomEventCriterion.createFlying("super_scare_entity")).build(consumer, "extra_spooky");
             p.child(Items.BLACK_CANDLE).frame(AdvancementFrame.CHALLENGE).criterion("screech_self", CustomEventCriterion.createFlying("screech_self")).build(consumer, "screech_self");
         });
     }
@@ -217,7 +220,7 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
     public static CriterionConditions dragonScroll(boolean receiving, ItemPredicate items) {
         return new SendViaDragonBreathScrollCriterion.Conditions(
                 LootContextPredicate.EMPTY,
-                ItemPredicate.ANY,
+                items,
                 receiving,
                 Optional.empty(),
                 TriState.DEFAULT,
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java
index d6f032c8..57cbc2ef 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java
@@ -380,6 +380,9 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
                 getWorld().emitGameEvent(player, GameEvent.EQUIP, getBlockPos());
             }
             setDesign(HotAirBalloonItem.getDesign(getWorld(), stack));
+            if (hasBurner() && hasBalloon()) {
+                UCriteria.CONSTRUCT_BALLOON.trigger(player);
+            }
             return ActionResult.SUCCESS;
         }
 
@@ -405,6 +408,9 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
             if (!player.isSneaky()) {
                 getWorld().emitGameEvent(player, GameEvent.EQUIP, getBlockPos());
             }
+            if (hasBurner() && hasBalloon()) {
+                UCriteria.CONSTRUCT_BALLOON.trigger(player);
+            }
             return ActionResult.SUCCESS;
         }
 
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index 7599643d..f7497d0c 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -1598,6 +1598,8 @@
   "advancements.unicopia.take_a_note.description": "Obtain a dragon breath scroll",
   "advancements.unicopia.dear_princess.title": "Dear princess...",
   "advancements.unicopia.dear_princess.description": "Send a letter with a dragon's breath scroll",
+  "advancements.unicopia.i_await_your_reply.title": "I Await Your Reply",
+  "advancements.unicopia.i_await_your_reply.description": "Use the dragon's breath scroll to send someone a dragon's breath scroll",
   "advancements.unicopia.baked_bads.title": "Baked Bads",
   "advancements.unicopia.baked_bads.description": "Bake a delicious muffin",
   "advancements.unicopia.mid_flight_interruption.title": "Mid-Flight Interruption",
@@ -1651,6 +1653,10 @@
   "advancements.unicopia.sweet_apple_acres.description": "Obtain one of every apple",
   "advancements.unicopia.brew_cider.title": "Applejack's Finest",
   "advancements.unicopia.brew_cider.description": "Brew some cider",
+  "advancements.unicopia.basket_case.title": "Basket Case",
+  "advancements.unicopia.basket_case.description": "Weave a basket",
+  "advancements.unicopia.aeronaut.title": "Aeronaut",
+  "advancements.unicopia.aeronaut.description": "Equip your basket with a lantern and hot air balloon",
   "advancements.unicopia.travelling_in_style.title": "Travelling in Style",
   "advancements.unicopia.travelling_in_style.description": "Ride a hot air balloon",
 
@@ -1672,6 +1678,14 @@
   "advancements.unicopia.earth_route.description": "Join the Apple Clan",
   "advancements.unicopia.sticks_and_stones.title": "Sticks and Stones",
   "advancements.unicopia.sticks_and_stones.description": "Kill a mob by throwing rocks at it",
+  "advancements.unicopia.blacksmith.title": "Blacksmith",
+  "advancements.unicopia.blacksmith.description": "Craft a horseshoe",
+  "advancements.unicopia.change_of_shoes.title": "A Change of Shoes",
+  "advancements.unicopia.change_of_shoes.description": "Craft an iron horse shoe",
+  "advancements.unicopia.fashionably_expensive.title": "Fashionably Expensive",
+  "advancements.unicopia.fashionably_expensive.description": "Upgrade to a set of golden horse shoes",
+  "advancements.unicopia.overkill.title": "Overkill",
+  "advancements.unicopia.overkill.description": "Craft a netherite horse shoe",
   "advancements.unicopia.dead_ringer.title": "Dead Ringer",
   "advancements.unicopia.dead_ringer.description": "Kill a mob with a horseshoe",
   "advancements.unicopia.born_on_a_rock_farm.title": "Born on a Rock Farm",

From 9ef3a946f6b3c6e389f82652c60de21fe220810c Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 00:41:10 +0000
Subject: [PATCH 03/14] Cache computed shapes (should improve bed performance)

---
 .../com/minelittlepony/unicopia/block/FancyBedBlock.java     | 1 -
 .../com/minelittlepony/unicopia/util/VoxelShapeUtil.java     | 5 +++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/block/FancyBedBlock.java b/src/main/java/com/minelittlepony/unicopia/block/FancyBedBlock.java
index bc26712f..f2fa1820 100644
--- a/src/main/java/com/minelittlepony/unicopia/block/FancyBedBlock.java
+++ b/src/main/java/com/minelittlepony/unicopia/block/FancyBedBlock.java
@@ -31,7 +31,6 @@ import net.minecraft.util.shape.VoxelShape;
 import net.minecraft.util.shape.VoxelShapes;
 import net.minecraft.world.BlockView;
 import net.minecraft.world.World;
-import net.minecraft.world.WorldAccess;
 import net.minecraft.world.event.GameEvent;
 
 public class FancyBedBlock extends BedBlock {
diff --git a/src/main/java/com/minelittlepony/unicopia/util/VoxelShapeUtil.java b/src/main/java/com/minelittlepony/unicopia/util/VoxelShapeUtil.java
index adff046a..0a14890d 100644
--- a/src/main/java/com/minelittlepony/unicopia/util/VoxelShapeUtil.java
+++ b/src/main/java/com/minelittlepony/unicopia/util/VoxelShapeUtil.java
@@ -1,5 +1,6 @@
 package com.minelittlepony.unicopia.util;
 
+import net.minecraft.util.Util;
 import net.minecraft.util.math.Box;
 import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.MathHelper;
@@ -14,7 +15,7 @@ public interface VoxelShapeUtil {
     Vec3d CENTER = new Vec3d(0.5, 0, 0.5);
 
     static Function<Direction, VoxelShape> rotator(VoxelShape base) {
-        return d -> rotate(base, d);
+        return Util.memoize(d -> rotate(base, d));
     }
 
     static VoxelShape rotate(VoxelShape shape, Direction direction) {
@@ -27,7 +28,7 @@ public interface VoxelShapeUtil {
         float angle = direction.asRotation() * MathHelper.RADIANS_PER_DEGREE;
         return VoxelShapes.union(VoxelShapes.empty(), shape.getBoundingBoxes().stream()
             .map(box -> {
-              //These first two are enough for orthogonal rotations
+                //These first two are enough for orthogonal rotations
                 Vec3d a = rotate(box.minX, box.minZ, angle);
                 Vec3d b = rotate(box.maxX, box.maxZ, angle);
                 //These cover odd angles

From a6b8e6fe93245c27af9f64711b7a94747a127943 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 00:41:42 +0000
Subject: [PATCH 04/14] You can now place cloud pillars horizontally

---
 .../block/cloud/CloudPillarBlock.java         | 92 +++++++++++++------
 .../providers/UBlockStateModelGenerator.java  | 14 ++-
 2 files changed, 75 insertions(+), 31 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudPillarBlock.java b/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudPillarBlock.java
index 69b372b8..baeb2f16 100644
--- a/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudPillarBlock.java
+++ b/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudPillarBlock.java
@@ -1,79 +1,115 @@
 package com.minelittlepony.unicopia.block.cloud;
 
 import java.util.Map;
+import java.util.function.Function;
 
 import org.jetbrains.annotations.Nullable;
 
 import com.minelittlepony.unicopia.EquineContext;
-
 import net.minecraft.block.Block;
 import net.minecraft.block.BlockState;
+import net.minecraft.block.PillarBlock;
 import net.minecraft.block.ShapeContext;
 import net.minecraft.item.ItemPlacementContext;
 import net.minecraft.state.StateManager;
 import net.minecraft.state.property.BooleanProperty;
+import net.minecraft.state.property.EnumProperty;
 import net.minecraft.state.property.Properties;
+import net.minecraft.util.BlockRotation;
+import net.minecraft.util.Util;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Direction;
+import net.minecraft.util.math.Direction.AxisDirection;
 import net.minecraft.util.shape.VoxelShape;
 import net.minecraft.util.shape.VoxelShapes;
 import net.minecraft.world.BlockView;
 import net.minecraft.world.WorldAccess;
 
 public class CloudPillarBlock extends CloudBlock {
-    private static final BooleanProperty NORTH = Properties.NORTH;
-    private static final BooleanProperty SOUTH = Properties.SOUTH;
+    public static final EnumProperty<Direction.Axis> AXIS = Properties.AXIS;
+    private static final BooleanProperty TOP = Properties.NORTH;
+    private static final BooleanProperty BOTTOM = Properties.SOUTH;
     private static final Map<Direction, BooleanProperty> DIRECTION_PROPERTIES = Map.of(
-            Direction.UP, NORTH,
-            Direction.DOWN, SOUTH
+            Direction.UP, TOP, Direction.DOWN, BOTTOM,
+            Direction.SOUTH, TOP, Direction.NORTH, BOTTOM,
+            Direction.EAST, TOP, Direction.WEST, BOTTOM
     );
-
-    private static final VoxelShape CORE_SHAPE = Block.createCuboidShape(1, 0, 1, 15, 16, 15);
-    private static final VoxelShape FOOT_SHAPE = Block.createCuboidShape(0, 0, 0, 16, 5, 16);
-    private static final VoxelShape CAP_SHAPE = FOOT_SHAPE.offset(0, 11F / 16F, 0);
-
-    private static final VoxelShape[] SHAPES = new VoxelShape[] {
-                          CORE_SHAPE,
-        VoxelShapes.union(CORE_SHAPE, FOOT_SHAPE),
-        VoxelShapes.union(CORE_SHAPE,             CAP_SHAPE),
-        VoxelShapes.union(CORE_SHAPE, FOOT_SHAPE, CAP_SHAPE)
-    };
-    // [0,0] [0,1]
-    // [1,0] [1,1]
+    private static final Function<Direction.Axis, VoxelShape[]> SHAPES = Util.memoize(axis -> {
+        int[] offsets = { axis.choose(1, 0, 0), axis.choose(0, 1, 0), axis.choose(0, 0, 1) };
+        float capOffset = 11F / 16F;
+        VoxelShape core = Block.createCuboidShape(
+            axis.choose(0, 1, 1), axis.choose(1, 0, 1), axis.choose(1, 1, 0),
+            16 - axis.choose(0, 1, 1), 16 - axis.choose(1, 0, 1), 16 - axis.choose(1, 1, 0)
+        );
+        VoxelShape foot = Block.createCuboidShape(0, 0, 0, 16 - (11 * offsets[0]), 16 - (11 * offsets[1]), 16 - (11 * offsets[2]));
+        VoxelShape cap = foot.offset(capOffset * offsets[0], capOffset * offsets[1], capOffset * offsets[2]);
+        return new VoxelShape[] {
+                core,
+                VoxelShapes.union(core, foot),
+                VoxelShapes.union(core, cap),
+                VoxelShapes.union(core, cap, foot)
+        };
+    });
 
     public CloudPillarBlock(Settings settings) {
         super(settings, false);
-        setDefaultState(getDefaultState().with(NORTH, true).with(SOUTH, true));
+        setDefaultState(getDefaultState().with(TOP, true).with(BOTTOM, true).with(AXIS, Direction.Axis.Y));
     }
 
     @Override
     protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
-        builder.add(NORTH, SOUTH);
+        builder.add(AXIS, TOP, BOTTOM);
     }
 
     @Override
     protected VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context, EquineContext equineContext) {
-        return SHAPES[(state.get(NORTH) ? 0 : 2) + (state.get(SOUTH) ? 0 : 1)];
+        var axis = state.get(AXIS);
+
+        int[] offsets = { axis.choose(1, 0, 0), axis.choose(0, 1, 0), axis.choose(0, 0, 1) };
+        float capOffset = 11F / 16F;
+        VoxelShape core = Block.createCuboidShape(
+                axis.choose(0, 1, 1), axis.choose(1, 0, 1), axis.choose(1, 1, 0),
+                16 - axis.choose(0, 1, 1), 16 - axis.choose(1, 0, 1), 16 - axis.choose(1, 1, 0)
+            );
+        VoxelShape foot = Block.createCuboidShape(0, 0, 0, 16 - (11 * offsets[0]), 16 - (11 * offsets[1]), 16 - (11 * offsets[2]));
+        VoxelShape cap = foot.offset(capOffset * offsets[0], capOffset * offsets[1], capOffset * offsets[2]);
+        var temp = new VoxelShape[] {
+                core,
+                VoxelShapes.union(core, foot),
+                VoxelShapes.union(core, cap),
+                VoxelShapes.union(core, cap, foot)
+        };
+        return temp[(state.get(TOP) ? 0 : 2) + (state.get(BOTTOM) ? 0 : 1)];
+        //return SHAPES.apply(state.get(AXIS))[(state.get(TOP) ? 0 : 2) + (state.get(BOTTOM) ? 0 : 1)];
     }
 
     @Override
     @Nullable
     protected BlockState getPlacementState(ItemPlacementContext placementContext, EquineContext equineContext) {
         BlockPos pos = placementContext.getBlockPos();
-        BlockState state = super.getPlacementState(placementContext, equineContext);
-        for (var property : DIRECTION_PROPERTIES.entrySet()) {
-            state = state.with(property.getValue(), placementContext.getWorld().getBlockState(pos.offset(property.getKey())).isOf(this));
-        }
-        return state;
+        Direction.Axis axis = placementContext.getSide().getAxis();
+        Direction upDirection = Direction.get(AxisDirection.POSITIVE, axis);
+        Direction downDirection = Direction.get(AxisDirection.NEGATIVE, axis);
+        BlockState above = placementContext.getWorld().getBlockState(pos.offset(upDirection));
+        BlockState below = placementContext.getWorld().getBlockState(pos.offset(downDirection));
+        return super.getPlacementState(placementContext, equineContext)
+                .with(DIRECTION_PROPERTIES.get(upDirection), above.isOf(this) && above.get(AXIS) == axis)
+                .with(DIRECTION_PROPERTIES.get(downDirection), below.isOf(this) && below.get(AXIS) == axis)
+                .with(AXIS, axis);
     }
 
     @Deprecated
     @Override
     public BlockState getStateForNeighborUpdate(BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos) {
-        if (direction.getAxis() == Direction.Axis.Y) {
-            return state.with(DIRECTION_PROPERTIES.get(direction), neighborState.isOf(this));
+        if (direction.getAxis() == state.get(AXIS)) {
+            return state.with(DIRECTION_PROPERTIES.get(direction), neighborState.isOf(this) && neighborState.get(AXIS) == state.get(AXIS));
         }
 
         return state;
     }
+
+    @Override
+    public BlockState rotate(BlockState state, BlockRotation rotation) {
+        return PillarBlock.changeRotation(state, rotation);
+    }
 }
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java
index 2b86e3d2..283e25ef 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java
@@ -381,9 +381,17 @@ public class UBlockStateModelGenerator extends BlockStateModelGenerator {
         Identifier middle = BlockModels.TEMPLATE_PILLAR.upload(pillar, textures, modelCollector);
         Identifier end = BlockModels.TEMPLATE_PILLAR_END.upload(pillar, textures, modelCollector);
         blockStateCollector.accept(MultipartBlockStateSupplier.create(pillar)
-                .with(BlockStateVariant.create().put(MODEL, middle))
-                .with(When.create().set(Properties.NORTH, false), BlockStateVariant.create().put(MODEL, end).put(UVLOCK, true).put(X, R180))
-                .with(When.create().set(Properties.SOUTH, false), BlockStateVariant.create().put(MODEL, end))
+                .with(When.create().set(Properties.AXIS, Direction.Axis.X), BlockStateVariant.create().put(MODEL, middle).put(X, R90).put(Y, R90))
+                .with(When.create().set(Properties.AXIS, Direction.Axis.X).set(Properties.NORTH, false), BlockStateVariant.create().put(MODEL, end).put(X, R270).put(Y, R90))
+                .with(When.create().set(Properties.AXIS, Direction.Axis.X).set(Properties.SOUTH, false), BlockStateVariant.create().put(MODEL, end).put(X, R90).put(Y, R90))
+
+                .with(When.create().set(Properties.AXIS, Direction.Axis.Y), BlockStateVariant.create().put(MODEL, middle))
+                .with(When.create().set(Properties.AXIS, Direction.Axis.Y).set(Properties.NORTH, false), BlockStateVariant.create().put(MODEL, end).put(X, R180))
+                .with(When.create().set(Properties.AXIS, Direction.Axis.Y).set(Properties.SOUTH, false), BlockStateVariant.create().put(MODEL, end))
+
+                .with(When.create().set(Properties.AXIS, Direction.Axis.Z), BlockStateVariant.create().put(MODEL, middle).put(X, R90))
+                .with(When.create().set(Properties.AXIS, Direction.Axis.Z).set(Properties.NORTH, false), BlockStateVariant.create().put(MODEL, end).put(X, R90))
+                .with(When.create().set(Properties.AXIS, Direction.Axis.Z).set(Properties.SOUTH, false), BlockStateVariant.create().put(MODEL, end).put(X, R270))
         );
         ItemModels.TEMPLATE_PILLAR.upload(ModelIds.getItemModelId(pillar.asItem()), textures, modelCollector);
     }

From c91b5ac985cdc0faf1fcce5d922e2a6c69de1dd4 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 18:06:50 +0000
Subject: [PATCH 05/14] Smol todo

---
 .../unicopia/datagen/providers/UAdvancementsProvider.java      | 3 ++-
 src/main/resources/assets/unicopia/lang/en_us.json             | 2 ++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
index fc831f33..994082f4 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
@@ -3,7 +3,6 @@ package com.minelittlepony.unicopia.datagen.providers;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
-
 import com.minelittlepony.unicopia.Race;
 import com.minelittlepony.unicopia.UTags;
 import com.minelittlepony.unicopia.advancement.CustomEventCriterion;
@@ -155,6 +154,8 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
 
     private void generateUnicornTribeAdvancementsTree(Consumer<Advancement> consumer, AdvancementDisplayBuilder.Parent parent) {
         parent.child(UItems.SPELLBOOK).criterion("has_spellbook", hasItems(UItems.SPELLBOOK)).build(consumer, "books").children(p -> {
+            //ItemPredicate bookPredicate = ItemPredicate.Builder.create().tag(ItemTags.BOOKSHELF_BOOKS).build();
+            //p.child(Items.BOOK).hidden().frame(AdvancementFrame.CHALLENGE).criterion("has_books", InventoryChangedCriterion.Conditions.items(IntStream.range(0, 9 * 4).mapToObj(i -> bookPredicate).toArray(ItemPredicate[]::new))).build(consumer, "books_books_books");
             p.child(UItems.CRYSTAL_SHARD).criterion("has_shard", hasItems(UItems.CRYSTAL_SHARD)).build(consumer, "crystaline").children(pp -> {
                 pp.child(UItems.CRYSTAL_HEART).criterion("power_up_heart", CustomEventCriterion.create("power_up_heart")).rewards(AdvancementRewards.Builder.experience(105)).build(consumer, "power_up_heart");
             });
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index f7497d0c..d9c8f395 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -1731,6 +1731,8 @@
   
   "advancements.unicopia.books.title": "Books!",
   "advancements.unicopia.books.description": "This is MY spellbook and I'm going to READ it!",
+  "advancements.unicopia.books_books_books.title": "Books! Books! Books!",
+  "advancements.unicopia.books_books_books.description": "Have an inventory full of books",
   "advancements.unicopia.tempted.title": "Tempting...",
   "advancements.unicopia.tempted.description": "Put on the alicorn amulet",
   "advancements.unicopia.hello_darkness_my_old_friend.title": "Hello Darkness...",

From fd03dabdd67b84a5e90be4dc981c4a9221eb2919 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 18:08:40 +0000
Subject: [PATCH 06/14] Added specter

---
 .../unicopia/client/URenderers.java           |   7 +-
 .../client/particle/FootprintParticle.java    |  74 +++++++++++
 .../client/particle/RunesParticle.java        | 123 ------------------
 .../unicopia/entity/mob/SpecterEntity.java    | 118 +++++++++++++++++
 .../unicopia/entity/mob/UEntities.java        |  12 +-
 .../particle/FootprintParticleEffect.java     |  42 ++++++
 .../unicopia/particle/UParticles.java         |   4 +-
 .../resources/assets/unicopia/lang/en_us.json |   1 +
 .../assets/unicopia/particles/footprint.json  |   5 +
 .../unicopia/textures/particle/footprint.png  | Bin 0 -> 6935 bytes
 10 files changed, 256 insertions(+), 130 deletions(-)
 create mode 100644 src/main/java/com/minelittlepony/unicopia/client/particle/FootprintParticle.java
 delete mode 100644 src/main/java/com/minelittlepony/unicopia/client/particle/RunesParticle.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/entity/mob/SpecterEntity.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/particle/FootprintParticleEffect.java
 create mode 100644 src/main/resources/assets/unicopia/particles/footprint.json
 create mode 100644 src/main/resources/assets/unicopia/textures/particle/footprint.png

diff --git a/src/main/java/com/minelittlepony/unicopia/client/URenderers.java b/src/main/java/com/minelittlepony/unicopia/client/URenderers.java
index 69d0ab9c..e5896bd6 100644
--- a/src/main/java/com/minelittlepony/unicopia/client/URenderers.java
+++ b/src/main/java/com/minelittlepony/unicopia/client/URenderers.java
@@ -12,6 +12,7 @@ import com.minelittlepony.unicopia.client.particle.CloudsEscapingParticle;
 import com.minelittlepony.unicopia.client.particle.DiskParticle;
 import com.minelittlepony.unicopia.client.particle.DustCloudParticle;
 import com.minelittlepony.unicopia.client.particle.FloatingBubbleParticle;
+import com.minelittlepony.unicopia.client.particle.FootprintParticle;
 import com.minelittlepony.unicopia.client.particle.GroundPoundParticle;
 import com.minelittlepony.unicopia.client.particle.HealthDrainParticle;
 import com.minelittlepony.unicopia.client.particle.LightningBoltParticle;
@@ -19,7 +20,6 @@ import com.minelittlepony.unicopia.client.particle.MagicParticle;
 import com.minelittlepony.unicopia.client.particle.RainboomParticle;
 import com.minelittlepony.unicopia.client.particle.RainbowTrailParticle;
 import com.minelittlepony.unicopia.client.particle.RaindropsParticle;
-import com.minelittlepony.unicopia.client.particle.RunesParticle;
 import com.minelittlepony.unicopia.client.particle.ShockwaveParticle;
 import com.minelittlepony.unicopia.client.particle.SphereParticle;
 import com.minelittlepony.unicopia.client.render.*;
@@ -54,6 +54,7 @@ import net.minecraft.client.particle.SpriteProvider;
 import net.minecraft.client.render.*;
 import net.minecraft.client.render.VertexConsumerProvider.Immediate;
 import net.minecraft.client.render.block.entity.BlockEntityRendererFactories;
+import net.minecraft.client.render.entity.EmptyEntityRenderer;
 import net.minecraft.client.render.entity.FlyingItemEntityRenderer;
 import net.minecraft.client.render.item.ItemRenderer;
 import net.minecraft.client.render.model.json.ModelTransformationMode;
@@ -68,7 +69,6 @@ import net.minecraft.util.Identifier;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.BlockRenderView;
 
-@SuppressWarnings("deprecation")
 public interface URenderers {
     BlockEntity CHEST_RENDER_ENTITY = new CloudChestBlock.TileData(BlockPos.ORIGIN, UBlocks.CLOUD_CHEST.getDefaultState());
 
@@ -78,10 +78,10 @@ public interface URenderers {
         ParticleFactoryRegistry.getInstance().register(UParticles.BUBBLE, createFactory(FloatingBubbleParticle::new));
         ParticleFactoryRegistry.getInstance().register(UParticles.RAIN_DROPS, createFactory(RaindropsParticle::new));
         ParticleFactoryRegistry.getInstance().register(UParticles.HEALTH_DRAIN, createFactory(HealthDrainParticle::create));
+        ParticleFactoryRegistry.getInstance().register(UParticles.FOOTPRINT, createFactory(FootprintParticle::new));
         ParticleFactoryRegistry.getInstance().register(UParticles.RAINBOOM_RING, RainboomParticle::new);
         ParticleFactoryRegistry.getInstance().register(UParticles.RAINBOOM_TRAIL, RainbowTrailParticle::new);
         ParticleFactoryRegistry.getInstance().register(UParticles.SHOCKWAVE, ShockwaveParticle::new);
-        ParticleFactoryRegistry.getInstance().register(UParticles.MAGIC_RUNES, RunesParticle::new);
         ParticleFactoryRegistry.getInstance().register(UParticles.SPHERE, SphereParticle::new);
         ParticleFactoryRegistry.getInstance().register(UParticles.DISK, DiskParticle::new);
         ParticleFactoryRegistry.getInstance().register(UParticles.GROUND_POUND, GroundPoundParticle::new);
@@ -111,6 +111,7 @@ public interface URenderers {
         EntityRendererRegistry.register(UEntities.LOOT_BUG, LootBugEntityRenderer::new);
         EntityRendererRegistry.register(UEntities.TENTACLE, TentacleEntityRenderer::new);
         EntityRendererRegistry.register(UEntities.IGNOMINIOUS_BULB, IgnominiousBulbEntityRenderer::new);
+        EntityRendererRegistry.register(UEntities.SPECTER, EmptyEntityRenderer::new);
 
         BlockEntityRendererFactories.register(UBlockEntities.WEATHER_VANE, WeatherVaneBlockEntityRenderer::new);
         BlockEntityRendererFactories.register(UBlockEntities.FANCY_BED, CloudBedBlockEntityRenderer::new);
diff --git a/src/main/java/com/minelittlepony/unicopia/client/particle/FootprintParticle.java b/src/main/java/com/minelittlepony/unicopia/client/particle/FootprintParticle.java
new file mode 100644
index 00000000..32075c5e
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/client/particle/FootprintParticle.java
@@ -0,0 +1,74 @@
+package com.minelittlepony.unicopia.client.particle;
+
+import org.joml.Vector3f;
+
+import com.minelittlepony.unicopia.particle.FootprintParticleEffect;
+
+import net.minecraft.client.particle.ParticleTextureSheet;
+import net.minecraft.client.particle.SpriteBillboardParticle;
+import net.minecraft.client.particle.SpriteProvider;
+import net.minecraft.client.render.Camera;
+import net.minecraft.client.render.VertexConsumer;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+
+public class FootprintParticle extends SpriteBillboardParticle {
+    // specter
+
+    public FootprintParticle(FootprintParticleEffect effect, SpriteProvider provider, ClientWorld world, double x, double y, double z, double dx, double dy, double dz) {
+        super(world, x, y, z, 0, 0, 0);
+        setVelocity(0, 0, 0);
+        setSprite(provider.getSprite(world.random));
+        this.angle = effect.yaw() * MathHelper.RADIANS_PER_DEGREE;
+        this.maxAge = 1000;
+        this.gravityStrength = 1;
+    }
+
+    @Override
+    public ParticleTextureSheet getType() {
+        return ParticleTextureSheet.PARTICLE_SHEET_TRANSLUCENT;
+    }
+
+    @Override
+    public void tick() {
+        super.tick();
+    }
+
+    @Override
+    public void buildGeometry(VertexConsumer drawer, Camera camera, float tickDelta) {
+        Vec3d cam = camera.getPos();
+
+        float renderX = (float)(MathHelper.lerp(tickDelta, prevPosX, x) - cam.getX());
+        float renderY = (float)(MathHelper.lerp(tickDelta, prevPosY, y) - cam.getY());
+        float renderZ = (float)(MathHelper.lerp(tickDelta, prevPosZ, z) - cam.getZ());
+
+        Vector3f[] corners = new Vector3f[]{
+                new Vector3f(-1, 0, -1),
+                new Vector3f(-1, 0, 1),
+                new Vector3f( 1, 0, 1),
+                new Vector3f( 1, 0, -1)
+        };
+        for (int k = 0; k < 4; ++k) {
+           Vector3f corner = corners[k];
+           corner.mul(0.2F);
+           corner.rotateAxis(angle, 0, 1, 0);
+           corner.add(renderX, renderY + 0.0001F, renderZ);
+        }
+
+        float alpha = this.alpha * (1 - ((float)age / maxAge));
+        int light = getBrightness(tickDelta);
+
+        float minU = this.sprite.getMinU();
+        float maxU = this.sprite.getMaxU();
+
+        float minV = this.sprite.getMinV();
+        float maxV = this.sprite.getMaxV();
+
+        drawer.vertex(corners[0].x, corners[0].y, corners[0].z).texture(minU, minV).color(red, green, blue, alpha).light(light).next();
+        drawer.vertex(corners[1].x, corners[1].y, corners[1].z).texture(maxU, minV).color(red, green, blue, alpha).light(light).next();
+        drawer.vertex(corners[2].x, corners[2].y, corners[2].z).texture(maxU, maxV).color(red, green, blue, alpha).light(light).next();
+        drawer.vertex(corners[3].x, corners[3].y, corners[3].z).texture(minU, maxV).color(red, green, blue, alpha).light(light).next();
+    }
+
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/client/particle/RunesParticle.java b/src/main/java/com/minelittlepony/unicopia/client/particle/RunesParticle.java
deleted file mode 100644
index 21294ca3..00000000
--- a/src/main/java/com/minelittlepony/unicopia/client/particle/RunesParticle.java
+++ /dev/null
@@ -1,123 +0,0 @@
-package com.minelittlepony.unicopia.client.particle;
-
-import org.joml.Quaternionf;
-import org.joml.Vector3f;
-
-import com.minelittlepony.unicopia.Unicopia;
-import com.minelittlepony.unicopia.particle.OrientedBillboardParticleEffect;
-import com.mojang.blaze3d.systems.RenderSystem;
-
-import net.minecraft.client.render.BufferBuilder;
-import net.minecraft.client.render.Tessellator;
-import net.minecraft.client.world.ClientWorld;
-import net.minecraft.util.Identifier;
-import net.minecraft.util.math.*;
-
-@Deprecated
-public class RunesParticle extends OrientedBillboardParticle {
-
-    private static final Identifier[] TEXTURES = new Identifier[] {
-            Unicopia.id("textures/particles/runes_0.png"),
-            Unicopia.id("textures/particles/runes_1.png"),
-            Unicopia.id("textures/particles/runes_2.png"),
-            Unicopia.id("textures/particles/runes_3.png"),
-            Unicopia.id("textures/particles/runes_4.png"),
-            Unicopia.id("textures/particles/runes_5.png")
-    };
-
-    protected float targetSize = 3;
-
-    protected float prevBaseSize = 0;
-    protected float baseSize = 0;
-
-    private float prevRotationAngle;
-    private float rotationAngle;
-
-    public RunesParticle(OrientedBillboardParticleEffect effect, ClientWorld world, double x, double y, double z, double velocityX, double velocityY, double velocityZ) {
-        super(effect, world, x, y, z, velocityX, velocityY, velocityZ);
-        setMaxAge(70);
-
-        red = world.random.nextFloat();
-        green = world.random.nextFloat();
-        blue = world.random.nextFloat();
-    }
-
-    @Override
-    public float getScale(float tickDelta) {
-       return MathHelper.lerp(tickDelta, prevBaseSize, baseSize) * super.getScale(tickDelta);
-    }
-
-    @Override
-    protected Identifier getTexture() {
-        return TEXTURES[0];
-    }
-
-    private float getAlphaScale() {
-        float transitionScale = age < maxAge / 2 ? 5 : 3;
-        return (float)Math.min(1, Math.sin(Math.PI * age / maxAge) * transitionScale);
-    }
-
-    @Override
-    protected int getBrightness(float tint) {
-        return 0xF000F0;
-    }
-
-    @Override
-    protected void renderQuads(Tessellator te, BufferBuilder buffer, float x, float y, float z, float tickDelta) {
-
-        float alpha = this.alpha * getAlphaScale();
-
-        float angle = MathHelper.lerp(tickDelta, prevRotationAngle, rotationAngle);
-
-        for (int i = 0; i < TEXTURES.length; i++) {
-            for (int dim = 0; dim < 3; dim++) {
-                RenderSystem.setShaderTexture(0, TEXTURES[i]);
-                RenderSystem.setShaderColor(red, green, blue, alpha / ((float)(dim * 3) + 1));
-
-                Vector3f[] corners = new Vector3f[]{
-                        new Vector3f(-1, -1, 0),
-                        new Vector3f(-1,  1, 0),
-                        new Vector3f( 1,  1, 0),
-                        new Vector3f( 1, -1, 0)
-                };
-                float scale = getScale(tickDelta);
-
-                float ringSpeed = (i % 2 == 0 ? i : -1) * i;
-
-                Quaternionf ringAngle = RotationAxis.POSITIVE_Z.rotationDegrees(angle * ringSpeed);
-                Quaternionf ringFlip = RotationAxis.POSITIVE_Y.rotationDegrees(angle * ringSpeed * dim);
-                Quaternionf ringRoll = RotationAxis.POSITIVE_X.rotationDegrees(angle * ringSpeed * dim);
-
-                for(int k = 0; k < 4; ++k) {
-                   Vector3f corner = corners[k];
-                   corner.rotate(ringAngle);
-                   corner.rotate(ringFlip);
-                   corner.rotate(ringRoll);
-                   corner.rotate(rotation);
-                   corner.mul(scale);
-                   corner.add(x, y + 0.001F, z);
-                }
-
-                renderQuad(te, buffer, corners, alpha, tickDelta);
-            }
-        }
-
-        RenderSystem.setShaderColor(1, 1, 1, 1);
-    }
-
-    @Override
-    public void tick() {
-        super.tick();
-
-        prevBaseSize = baseSize;
-        if (baseSize < targetSize) {
-            baseSize += 0.1F;
-        }
-        if (baseSize > targetSize) {
-            baseSize -= 0.1F;
-        }
-
-        rotationAngle = (rotationAngle + 0.3F) % 360;
-        prevRotationAngle = rotationAngle - 0.3F;
-    }
-}
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/SpecterEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/SpecterEntity.java
new file mode 100644
index 00000000..91f9b407
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/SpecterEntity.java
@@ -0,0 +1,118 @@
+package com.minelittlepony.unicopia.entity.mob;
+
+import org.jetbrains.annotations.Nullable;
+
+import com.minelittlepony.unicopia.particle.FootprintParticleEffect;
+import com.minelittlepony.unicopia.particle.ParticleUtils;
+
+import net.minecraft.entity.EntityType;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.ai.goal.ActiveTargetGoal;
+import net.minecraft.entity.ai.goal.LookAroundGoal;
+import net.minecraft.entity.ai.goal.LookAtEntityGoal;
+import net.minecraft.entity.ai.goal.MeleeAttackGoal;
+import net.minecraft.entity.ai.goal.RevengeGoal;
+import net.minecraft.entity.ai.goal.SwimGoal;
+import net.minecraft.entity.ai.goal.WanderAroundFarGoal;
+import net.minecraft.entity.attribute.DefaultAttributeContainer;
+import net.minecraft.entity.attribute.EntityAttributes;
+import net.minecraft.entity.damage.DamageSource;
+import net.minecraft.entity.mob.HostileEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.particle.BlockStateParticleEffect;
+import net.minecraft.particle.ParticleTypes;
+import net.minecraft.registry.tag.BlockTags;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.GameRules;
+import net.minecraft.world.World;
+
+public class SpecterEntity extends HostileEntity {
+    public static DefaultAttributeContainer.Builder createAttributes() {
+        return HostileEntity.createHostileAttributes()
+                .add(EntityAttributes.GENERIC_MAX_HEALTH, 16F)
+                .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.5F);
+    }
+
+    private double stepDistance;
+    private double nextStepDistance;
+    private boolean wasLeft;
+
+    public SpecterEntity(EntityType<? extends HostileEntity> entityType, World world) {
+        super(entityType, world);
+    }
+
+    @Override
+    protected void initGoals() {
+        this.goalSelector.add(1, new SwimGoal(this));
+        this.goalSelector.add(4, new MeleeAttackGoal(this, 1.0, true));
+        this.goalSelector.add(5, new WanderAroundFarGoal(this, 0.8));
+        this.goalSelector.add(6, new LookAtEntityGoal(this, PlayerEntity.class, 8.0f));
+        this.goalSelector.add(6, new LookAroundGoal(this));
+        this.targetSelector.add(1, new RevengeGoal(this));
+        this.targetSelector.add(2, new TargetGoal<>(this, PlayerEntity.class));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void tick() {
+        Vec3d prevPosition = getPos();
+        super.tick();
+        if (getBrightnessAtEyes() < 0.5F || getTarget() != null) {
+            ParticleUtils.spawnParticles(ParticleTypes.AMBIENT_ENTITY_EFFECT, this, 6);
+
+            if (getWorld().getGameRules().getBoolean(GameRules.DO_MOB_GRIEFING)) {
+                if (getWorld().getBlockState(getBlockPos()).isIn(BlockTags.REPLACEABLE_BY_TREES)) {
+                    getWorld().breakBlock(getBlockPos(), true);
+                }
+            }
+        }
+
+        if (!hasVehicle() && isOnGround()) {
+            stepDistance += getPos().subtract(prevPosition).horizontalLength() * 0.6F;
+            if (stepDistance >= nextStepDistance) {
+                nextStepDistance = stepDistance + 1;
+                wasLeft = !wasLeft;
+                float offset = 0.4F;
+                float yaw = getHeadYaw();
+                Vec3d offsetVec = new Vec3d((wasLeft ? offset : -offset), 0, 0).rotateY(yaw);
+                getWorld().addParticle(new FootprintParticleEffect(yaw), true, getX() + offsetVec.getX(), getY(), getZ() + offsetVec.getZ(), 0, 0, 0);
+                ParticleUtils.spawnParticles(new BlockStateParticleEffect(ParticleTypes.BLOCK, getSteppingBlockState()), getWorld(), getPos(), 6);
+                playSound(getSteppingBlockState().getSoundGroup().getStepSound(), 0.5F, 1);
+            }
+        }
+    }
+
+    @Override
+    public float getSoundPitch() {
+        return super.getSoundPitch() * 0.3F;
+    }
+
+    @Override
+    @Nullable
+    protected SoundEvent getHurtSound(DamageSource source) {
+        return null;
+    }
+
+    @Override
+    protected void playSwimSound(float volume) {
+
+    }
+
+    @Override
+    protected void onSwimmingStart() {
+
+    }
+
+    static class TargetGoal<T extends LivingEntity> extends ActiveTargetGoal<T> {
+        public TargetGoal(SpecterEntity specter, Class<T> targetEntityClass) {
+            super(specter, targetEntityClass, true);
+        }
+
+        @SuppressWarnings("deprecation")
+        @Override
+        public boolean canStart() {
+            return mob.getBrightnessAtEyes() < 0.5F && super.canStart();
+        }
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
index 924ca810..407710ef 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
@@ -7,7 +7,6 @@ import com.minelittlepony.unicopia.entity.behaviour.EntityBehaviour;
 import com.minelittlepony.unicopia.projectile.MagicBeamEntity;
 import com.minelittlepony.unicopia.projectile.MagicProjectileEntity;
 import com.minelittlepony.unicopia.projectile.PhysicsBodyProjectileEntity;
-
 import net.fabricmc.fabric.api.biome.v1.BiomeModifications;
 import net.fabricmc.fabric.api.biome.v1.BiomeSelectionContext;
 import net.fabricmc.fabric.api.biome.v1.BiomeSelectors;
@@ -17,10 +16,13 @@ import net.minecraft.entity.Entity;
 import net.minecraft.entity.EntityDimensions;
 import net.minecraft.entity.EntityType;
 import net.minecraft.entity.SpawnGroup;
+import net.minecraft.entity.SpawnRestriction.Location;
 import net.minecraft.entity.decoration.painting.PaintingVariant;
 import net.minecraft.entity.mob.FlyingEntity;
+import net.minecraft.entity.mob.HostileEntity;
 import net.minecraft.registry.*;
 import net.minecraft.registry.tag.BiomeTags;
+import net.minecraft.world.Heightmap.Type;
 
 public interface UEntities {
     EntityType<ButterflyEntity> BUTTERFLY = register("butterfly", FabricEntityTypeBuilder.create(SpawnGroup.AMBIENT, ButterflyEntity::new)
@@ -74,6 +76,11 @@ public interface UEntities {
     EntityType<IgnominiousBulbEntity> IGNOMINIOUS_BULB = register("ignominious_bulb", FabricEntityTypeBuilder.<IgnominiousBulbEntity>create(SpawnGroup.MISC, IgnominiousBulbEntity::new)
             .trackRangeChunks(8)
             .dimensions(EntityDimensions.fixed(3, 2)));
+    EntityType<SpecterEntity> SPECTER = register("specter", FabricEntityTypeBuilder.createMob().spawnGroup(SpawnGroup.MONSTER).entityFactory(SpecterEntity::new)
+            .spawnRestriction(Location.ON_GROUND, Type.WORLD_SURFACE, HostileEntity::canSpawnInDark)
+            .fireImmune()
+            .spawnableFarFromPlayer()
+            .dimensions(EntityDimensions.fixed(1, 2)));
 
     static <T extends Entity> EntityType<T> register(String name, FabricEntityTypeBuilder<T> builder) {
         EntityType<T> type = builder.build();
@@ -89,6 +96,7 @@ public interface UEntities {
         FabricDefaultAttributeRegistry.register(FRIENDLY_CREEPER, FriendlyCreeperEntity.createCreeperAttributes());
         FabricDefaultAttributeRegistry.register(LOOT_BUG, LootBugEntity.createSilverfishAttributes());
         FabricDefaultAttributeRegistry.register(IGNOMINIOUS_BULB, IgnominiousBulbEntity.createMobAttributes());
+        FabricDefaultAttributeRegistry.register(SPECTER, SpecterEntity.createAttributes());
 
         if (!Unicopia.getConfig().disableButterflySpawning.get()) {
             final Predicate<BiomeSelectionContext> butterflySpawnable = BiomeSelectors.foundInOverworld()
@@ -105,6 +113,8 @@ public interface UEntities {
             ), SpawnGroup.AMBIENT, BUTTERFLY, 7, 5, 19);
         }
 
+        BiomeModifications.addSpawn(BiomeSelectors.spawnsOneOf(EntityType.ZOMBIE), SpawnGroup.MONSTER, SPECTER, 2, 1, 2);
+
         UTradeOffers.bootstrap();
         EntityBehaviour.bootstrap();
         UEntityAttributes.bootstrap();
diff --git a/src/main/java/com/minelittlepony/unicopia/particle/FootprintParticleEffect.java b/src/main/java/com/minelittlepony/unicopia/particle/FootprintParticleEffect.java
new file mode 100644
index 00000000..69ef2300
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/particle/FootprintParticleEffect.java
@@ -0,0 +1,42 @@
+package com.minelittlepony.unicopia.particle;
+
+import java.util.Locale;
+
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+
+import net.minecraft.particle.ParticleEffect;
+import net.minecraft.particle.ParticleType;
+import net.minecraft.network.PacketByteBuf;
+import net.minecraft.registry.Registries;
+
+public record FootprintParticleEffect (
+        float yaw
+    ) implements ParticleEffect {
+    @SuppressWarnings("deprecation")
+    public static final ParticleEffect.Factory<FootprintParticleEffect> FACTORY = ParticleFactoryHelper.of(FootprintParticleEffect::new, FootprintParticleEffect::new);
+
+    protected FootprintParticleEffect(ParticleType<FootprintParticleEffect> type, StringReader reader) throws CommandSyntaxException {
+        this(ParticleFactoryHelper.readFloat(reader));
+    }
+
+    protected FootprintParticleEffect(ParticleType<FootprintParticleEffect> particleType, PacketByteBuf buf) {
+        this(buf.readFloat());
+    }
+
+    @Override
+    public ParticleType<?> getType() {
+        return UParticles.FOOTPRINT;
+    }
+
+    @Override
+    public void write(PacketByteBuf buf) {
+        buf.writeFloat(yaw);
+    }
+
+    @Override
+    public String asString() {
+        return String.format(Locale.ROOT, "%s %.2f", Registries.PARTICLE_TYPE.getId(getType()), yaw);
+    }
+
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java b/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java
index 9bec2e5c..da302010 100644
--- a/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java
+++ b/src/main/java/com/minelittlepony/unicopia/particle/UParticles.java
@@ -14,14 +14,12 @@ public interface UParticles {
     ParticleType<MagicParticleEffect> UNICORN_MAGIC = register("unicorn_magic", FabricParticleTypes.complex(MagicParticleEffect.FACTORY));
     DefaultParticleType CHANGELING_MAGIC = register("changeling_magic", FabricParticleTypes.simple());
     DefaultParticleType BUBBLE = register("bubble", FabricParticleTypes.simple());
+    ParticleType<FootprintParticleEffect> FOOTPRINT = register("footprint", FabricParticleTypes.complex(FootprintParticleEffect.FACTORY));
     ParticleType<BlockStateParticleEffect> DUST_CLOUD = register("dust_cloud", FabricParticleTypes.complex(BlockStateParticleEffect.PARAMETERS_FACTORY));
 
     ParticleType<OrientedBillboardParticleEffect> RAINBOOM_RING = register("rainboom_ring", FabricParticleTypes.complex(OrientedBillboardParticleEffect.FACTORY));
     ParticleType<TargetBoundParticleEffect> RAINBOOM_TRAIL = register("rainboom_trail", FabricParticleTypes.complex(TargetBoundParticleEffect.FACTORY));
 
-    @Deprecated
-    ParticleType<OrientedBillboardParticleEffect> MAGIC_RUNES = register("magic_runes", FabricParticleTypes.complex(OrientedBillboardParticleEffect.FACTORY));
-
     DefaultParticleType RAIN_DROPS = register("rain_drops", FabricParticleTypes.simple());
 
     ParticleType<SphereParticleEffect> SPHERE = register("sphere", FabricParticleTypes.complex(true, SphereParticleEffect.FACTORY));
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index d9c8f395..01a3726f 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -355,6 +355,7 @@
 
   "entity.unicopia.butterfly": "Butterfly",
   "entity.unicopia.twittermite": "Twittermite",
+  "entity.unicopia.specter": "Specter",
   "entity.unicopia.cast_spell": "Cast Spell",
   "entity.unicopia.cast_spell.by": "a spell cast by %s",
   "entity.unicopia.spellbook": "Spellbook",
diff --git a/src/main/resources/assets/unicopia/particles/footprint.json b/src/main/resources/assets/unicopia/particles/footprint.json
new file mode 100644
index 00000000..50c757f1
--- /dev/null
+++ b/src/main/resources/assets/unicopia/particles/footprint.json
@@ -0,0 +1,5 @@
+{
+  "textures": [
+    "unicopia:footprint"
+  ]
+}
diff --git a/src/main/resources/assets/unicopia/textures/particle/footprint.png b/src/main/resources/assets/unicopia/textures/particle/footprint.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0ea4822b22157f122b708ac5ca0d3ab18ba4a0b
GIT binary patch
literal 6935
zcmeHLd010d7JpetLI6uJI*2GCxI}75^729!K|m5E3KlRTN@wg#@&ZA!kc7=@6~_fr
z2M459sa6S6DOKxQt9)&>RcY0_wANZXwWzeUV@JB!IxdwtHw46)`Mw$FoBn6=-Mn+p
zJ@<EhXSpwVS(!b3N*Fhai(y!pK0})e*KjmAEclH1sfWZcj*-gOTlD&Pi~~Bbq%kpC
zz>YRRrqdW01N6&bM1YKJ(1@Z1`LC-$9v+YzKo$jT&>k6$j)lQ>0}LEwK3v;iSXcD=
zdK=_3FgX6Vl8*T;`mR&-S=j<SK_*R*C&GRyPH3cZ4XzN#2#o^QNOAB_+x&nL0Aaz{
zeXJK7aqNo;5$%f!$$c?R0+|uui!#F+AP*T32aVo~nSu5O<R~_B(8s;!LQf|aK6SEw
zGB`pBf{&ihv2={XVzF6495$OB9L(W{^20(yLPCc{41R(?A}VI&h^Xjj!Kj3>0^#_$
z=xC90+<2*sAc&Z;>SPt3lprT?1VRfA4h{_ojSLHm#ABml@kf639Kv`U#EC%@Vssvj
z!K3vY!D4`C5Y7MU4F%?O1~Z7o<^*#?z+hiE$`pgnWCR5<nXtML)-fh8C?Zyt#u}VM
zvW2C5yt=N56E|ts;UT$azaJ$xx|RfUhyE^dSk&k-V@2cQ2}Pn(rPictb(5!L=%>yw
z%*>mWKYNbJOj!zxti^6mS$Rce)%-s$UABBh%}Q_mnuZtFzPN6Eqpx{O%PU)3Uv1mH
zXYal@_P=@H;Jfb~IeM(){SS_R^zpe*&VPF0vx}FnT<!Ym>u;`o`_~_S{ORV;zufx!
z?K>zg6ldS7H?n_<iwAMhnM?+gjpCxwD^TD(W>Bn*6_J+1CQAnk@oEl#QeD%o!@+U#
z-0z1NU1zyNM-i7t|A0dCNA}+Zw&Z_`tUs^^ab3hh88j#!gNLPHx9@vJn}d8_5q*xx
zw^#@QP2}@p3;8*z!8(36Jx9mK8gg`)7!)C(U=+dXTG+#e71{{UMw|s941_S)j13s{
zsel+aK!Fx%HvtTX{q%rezq@*vaIOJzS->w7nek!F8nB;+d?QdUj8U*a*90aph#BII
zfSAY);#>{@mSO`;d3Mm7kO2h_mh^e+vyR-Gqob?aM83Up#Me&=8TXCrtv>I`3$?Iq
z5(JBZHDfq<)Ehz51fH0GM#-?ijEWcesv|u)x*`i=PCFL}qYSJpMJ@s@lo=2$eIS{v
z(ZFbBk<ZJ@gAJolYMWpdJPg*Qpkch=^FEm#*8-pTDVJBYof82jgN}Jc+diD02=ma|
zSCAYGWVw)-YE+NV?XX)oAF_jZqRcRnKOERCRzsop;+l&h-yTBkMRF{-8Fn#8H)MU&
z(X+ZCP3tef=lWOYfFE5WAl5;2952j*uK`Uygfs=fXfR@dDJu;Gzpvg5BI4`<9Y4>q
z0?LxNyl^g(k9Z(W&%=@owxA+b^H3`P8lZ^k%@_g8LB)ZkL*Ib2J@PFm?h~+?p*oK0
zGl2`d6{t$6`0Ji;$k7SC1}NVcQ2Co{CI~##t{tc-LHwLdEf@(<B80GIu^8p2OXTyO
zIe8tpoIHmjLp>7?X4q^HB0*rFmZF0xX9+BGS|Niv9e)O|*elwCBN)J&!7GO~vj@;o
z3XyllXe8_fAXNVFJ79=3!*an;2LV*CL^L)6yA{AL6kNX&3)?U#<mM1VjxM$yspo*<
zGO+PBKwy5pHkhH~$Mk?P9|r0gh%ME-H%BK!CFisNA#~){E%SNDg?{4m#)olX5!XYg
ztaRk|3$PDI_!J2KmP1X!B)|zA{Bbp}0RrlM^D3BO7kf!$>bxS~mZk=9^8`fFcZU5$
zY0x6C2!XBtn4_>|gTU|PX$S~r4Acv;AfVevCOY7$s4zpphyNh*L1P?>LcbX(BR&nR
zGu|%(y$FIj@_S&yhX$K5s^_!rymO84)v?r?o}R5wPZu~{4vV#f!m!E>3+s|I-b;->
zI=7%jIFmi0Rb?*P_+73pdfpIkcI-LscFnq{?*wH<)$@Nh<?z1F&JN=zhmX#l#h#TG
zeh(`xcjkKPB6?or&pA@*{LAW!TO%vFu|<218}^K)wKh`bTz%||bB8Q>o%0O;k2P`e
zvKgBOA0?7HK22V*;H~F>7~kz)R7-1l20t`5eC#@V{*mEF<S(r~RkcZV@_7BE1=EwP
zXM$G797t%tckDpd&P|2O|5RdmS(Q1y;b8s^V_u@+iuFp3uehQ6^nq>8OINn#UEj9w
zsr&C{rxncpTJvG)mYp@DYr+N1wcNIy;mtGSj%t_xvS#FcS%;*4+L&2EtD-|%)0TDp
z{culb#ePd^{Eaco>QlR)w=L3EcP5-EzVOSfUAFzx7ax*4TgvY38za4|pWoc9t6kh=
z>%4NX`GaQ<eKkn><Cba5yE15v_l>;DlxD|q#(Voydm2A4cs6G16RB$tPW=*NuQ;S#
zx%<=POJ98Y#pmx7Ui|e}?8_frA5ml0+{3Uz1y)18CqFAwV{+IMNVCI8B~;p+{%^~q
zlu9RQnn!sAMyk+iPZoB!zatb_&B?-9iY#fCGo323X3Td{Gv-e>nC8zjsm;QaiQJ@0
z4FK3E4=Jd$mDt^y%4DG*R|7IqON0WyiDzE2Fdu%|r8``dfJh(`q+(sAwHy~t<O-5p
zW{W0QJ2?OWvt(hB$K%vUBo!4E2^I1LhpSK`Q>)byDK5crF<6M*Rdx?qDYm=g5JWGA
zmU5e1R;S16unQ0-X>^o%l7&K87xeM7IkU3*;qC4~79byzO42EjB}gSUn`Gb)w?|hF
zAOS-Ec8A+g<)kFJl-p6}GEus6%I=992w^t$?{}8DO8nuNO%kevvVo}^yviPOiGBn3
z??5RiwA!5hU6AaDEIn4sgJeC#4XyaY8At@S_v1cf-50yx7_73gG+KwL4AIkTlZD8?
z#_TXz%^JT*;$|Wdr_^H7q>ze<L@6aUs?}1loUlkOMyXmsl9mBbdb`_0+D#OK0`LSY
z;NTVuNhvI9u}rBZ#DrNX6&sa=TujQ8W?XKuC@jjv0T55Qtk9KY$v{>J$_!9ax!Nd8
zloDbErBsOtIq7Gk6jO;Nv)O1?t4S&0hccTqlN~M_3FWlf$U;iuv={mpP{K87+4^K5
zo*;do$u1#17TAz1%(U9eDjzHutTt+fheX+wDP=ObQYFW6nL@1~<PVJUD3=>L5n=kD
zX8bFtEE)&~5=){!1pvPuV$r0#DAMC_861w1WMThvQ2%lk949mBA+@B30?_^^BSYVl
z(UVfCM%qgs!DTtjR!h}?vPOqTkOcNn@(imR{8#x^y(el0RoXl2ot9YrM@b;?9|{d=
z>J7n7mQ!XwPr&M3G8K{bLJIC4fp+cFTmMZf7*&aClS-u!%N2x0Oc)g^)CGiCnP^PJ
zEx3X($xIKUyB!u!1?i&F3L%e>D`-#uGowJ{FVwh)tt*NsR0Sz67t5q#nam)QYY4SQ
zrg~B;LkW}o_8}%JO)^R@hrrY(rI?`1xR^A<fl<rMO0!vo%W+EiFzNp>L_#c;i*dz2
z_e3DXBni4L_7ycr@-JoYH5hOoh1B%w;QkD^drANOK9DnXXZ;Odfp+^1JplA$Cy$ih
z$LV^Uu1CtiBN;#LuE*(mqzpWg@#F6Lzom=&;E9p4!w<a*cphxO{H+F_CRxU5Q?ytQ
zdT)Mf>l)AmJ2PgxF)U;-8Z@kNLnJ8K9(|UMeT^|}5J6|Wf1?2YW>3*;(+sK;RNd*?
zXkF;BMddHN)?QdB=dx8n;ml>)1A@9KSI1Q*Z#hufIX$m^&CB23XxptmsXfk$*?xM{
zJ1N1bT2V{q4Qxw8IB(+Wn$2xmre*nhnlzNr8b9}`Q@0njH+&?2w@@`xr;Y5WD~;YX
z;<;D5rd>I`RQ*k+w)4ilUrwxE^zzs8yX_<1s<`uE?z}&3kY66;yZ7_0ltp{b?>JpB
z@8%^Mr#sYg?qDoWd&n-z{8-z4il{wzur609%${kOdHdI2hhuM?|3F=*T|F{-tFdG2
zZP%*vwJjG)XV3CdYpKg$T#wl?RJf#mN1BGtTgqu}>8Seb&6ijQ*>BxFye<1W_vsf}
zHk_?VbyR=HGTFjTte<w>b1S_f#Tj?0Sf{JH`|*?WRrIcgh@XdMyxH7nPd$7xfB)pE
zQ!5IixNYx#eQLMiFC!M-y7MeCcgRvseD+K7Fvj*g)``%yE1kvj_MZQCWP|bEou6JS
z2-*Gz`L&IgG@VHsre|LLzE#-DftbUee&hK0__if)uF*yKmOat^<<1?$*EXD8b>lge
zB&2QSB+Y_G$?98oC*%%#<G`-**Z1u^KcQ!0(#}n+WA!;X!h3&P@V0Z&{^s^L&eiPJ
zeG$A>>Ey)M&GMVX-29h*`oS}1H1!8%@%5Vc)-!Xi?%vVLIl3yGd~M_QyonoUb+tR*
zsebNn&nY9fPZ&qhcaG}XuwD{!bk)VE<fY?lXU8VJKYDl*%{RV4r`_qhc_S(5!y1lc
u?srv58>>oFcAY=ov331>BNGa~O8Ik0(ztw=btw8vL$8~z-7)EzCI0}P0ZkJC

literal 0
HcmV?d00001


From f0c5cdb156265f7362b3bf804cce7e9ab4387eed Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 18:09:16 +0000
Subject: [PATCH 07/14] Fix butterflies spawning in the middle of nowhere

---
 .../minelittlepony/unicopia/entity/mob/ButterflyEntity.java   | 4 ++++
 .../com/minelittlepony/unicopia/entity/mob/UEntities.java     | 3 ++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/ButterflyEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/ButterflyEntity.java
index 088529b2..d1a43978 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/mob/ButterflyEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/ButterflyEntity.java
@@ -72,6 +72,10 @@ public class ButterflyEntity extends AmbientEntity {
         return createMobAttributes().add(EntityAttributes.GENERIC_MAX_HEALTH, 2);
     }
 
+    public static boolean canSpawn(EntityType<? extends ButterflyEntity> type, WorldAccess world, SpawnReason spawnReason, BlockPos pos, Random random) {
+        return world.getBlockState(pos.down()).isIn(BlockTags.ANIMALS_SPAWNABLE_ON);
+    }
+
     @Override
     public float getSoundPitch() {
         return super.getSoundPitch() * 0.95F;
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java b/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
index 407710ef..9200384f 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
@@ -25,7 +25,8 @@ import net.minecraft.registry.tag.BiomeTags;
 import net.minecraft.world.Heightmap.Type;
 
 public interface UEntities {
-    EntityType<ButterflyEntity> BUTTERFLY = register("butterfly", FabricEntityTypeBuilder.create(SpawnGroup.AMBIENT, ButterflyEntity::new)
+    EntityType<ButterflyEntity> BUTTERFLY = register("butterfly", FabricEntityTypeBuilder.createMob().spawnGroup(SpawnGroup.AMBIENT).entityFactory(ButterflyEntity::new)
+            .spawnRestriction(Location.NO_RESTRICTIONS, Type.WORLD_SURFACE_WG, ButterflyEntity::canSpawn)
             .dimensions(EntityDimensions.fixed(0.25F, 0.25F)));
     EntityType<MagicProjectileEntity> THROWN_ITEM = register("thrown_item", FabricEntityTypeBuilder.<MagicProjectileEntity>create(SpawnGroup.MISC, MagicProjectileEntity::new)
             .trackRangeBlocks(100)

From 2ac6970e04a2360c60b0a8eb3a14c3d65435075c Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 18:09:48 +0000
Subject: [PATCH 08/14] Added block of worms

---
 .../minelittlepony/unicopia/block/UBlocks.java   |   4 ++++
 .../providers/UBlockStateModelGenerator.java     |   3 ++-
 .../datagen/providers/UBlockTagProvider.java     |   1 +
 .../providers/loot/UBlockLootTableProvider.java  |   3 ++-
 .../providers/recipe/URecipeProvider.java        |   3 +++
 .../resources/assets/unicopia/lang/en_us.json    |   1 +
 .../unicopia/textures/block/worm_block.png       | Bin 0 -> 7064 bytes
 .../resources/data/c/tags/items/raw_insects.json |   3 ++-
 8 files changed, 15 insertions(+), 3 deletions(-)
 create mode 100644 src/main/resources/assets/unicopia/textures/block/worm_block.png

diff --git a/src/main/java/com/minelittlepony/unicopia/block/UBlocks.java b/src/main/java/com/minelittlepony/unicopia/block/UBlocks.java
index 1c45b8a8..32e48568 100644
--- a/src/main/java/com/minelittlepony/unicopia/block/UBlocks.java
+++ b/src/main/java/com/minelittlepony/unicopia/block/UBlocks.java
@@ -36,6 +36,7 @@ import com.minelittlepony.unicopia.item.cloud.CloudBlockItem;
 import com.minelittlepony.unicopia.item.group.ItemGroupRegistry;
 import com.minelittlepony.unicopia.server.world.UTreeGen;
 import net.fabricmc.fabric.api.object.builder.v1.block.FabricBlockSettings;
+import net.fabricmc.fabric.api.registry.CompostingChanceRegistry;
 import net.fabricmc.fabric.api.registry.FlammableBlockRegistry;
 import net.fabricmc.fabric.api.registry.OxidizableBlocksRegistry;
 import net.fabricmc.fabric.api.registry.StrippableBlockRegistry;
@@ -253,6 +254,7 @@ public interface UBlocks {
 
     Block SPECTRAL_FIRE = register("spectral_fire", new SpectralFireBlock(Settings.copy(Blocks.SOUL_FIRE)));
 
+    Block WORM_BLOCK = register("worm_block", new FallingBlock(Settings.create().hardness(0.1F).resistance(0).requiresTool().sounds(BlockSoundGroup.MUD)), ItemGroups.NATURAL);
     EdibleBlock HAY_BLOCK = register("hay_block", new EdibleBlock(new Identifier("hay_block"), new Identifier("wheat"), true));
 
     private static <T extends Block> T register(String name, T item) {
@@ -328,6 +330,8 @@ public interface UBlocks {
         FlammableBlockRegistry.getDefaultInstance().add(BANANAS, 5, 20);
         FlammableBlockRegistry.getDefaultInstance().add(CURING_JOKE, 60, 100);
 
+        CompostingChanceRegistry.INSTANCE.add(WORM_BLOCK, 8F);
+
         UBlockEntities.bootstrap();
         EdibleBlock.bootstrap();
     }
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java
index 283e25ef..45274e75 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java
@@ -123,7 +123,7 @@ public class UBlockStateModelGenerator extends BlockStateModelGenerator {
         registerLog(UBlocks.STRIPPED_PALM_LOG).log(UBlocks.STRIPPED_PALM_LOG).wood(UBlocks.STRIPPED_PALM_WOOD);
         registerCubeAllModelTexturePool(UBlocks.PALM_PLANKS).family(UBlockFamilies.PALM);
         registerHangingSign(UBlocks.STRIPPED_PALM_LOG, UBlocks.PALM_HANGING_SIGN, UBlocks.PALM_WALL_HANGING_SIGN);
-        registerSimpleCubeAll(UBlocks.PALM_LEAVES);
+        registerSingleton(UBlocks.PALM_LEAVES, TexturedModel.LEAVES);
 
         // zap wood
         registerLog(UBlocks.ZAP_LOG)
@@ -170,6 +170,7 @@ public class UBlockStateModelGenerator extends BlockStateModelGenerator {
         // shells
         registerAll(UBlockStateModelGenerator::registerShell, UBlocks.CLAM_SHELL, UBlocks.TURRET_SHELL, UBlocks.SCALLOP_SHELL);
         // other
+        registerSimpleCubeAll(UBlocks.WORM_BLOCK);
         registerBuiltinWithParticle(UBlocks.WEATHER_VANE, UBlocks.WEATHER_VANE.asItem());
         registerWithStages(UBlocks.FROSTED_OBSIDIAN, Properties.AGE_3, BlockModels.CUBE_ALL, 0, 1, 2, 3);
         registerWithStagesBuiltinModels(UBlocks.ROCKS, Properties.AGE_7, 0, 1, 2, 3, 4, 5, 6, 7);
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockTagProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockTagProvider.java
index 2deff29f..cc6d6411 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockTagProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockTagProvider.java
@@ -48,6 +48,7 @@ public class UBlockTagProvider extends FabricTagProvider.BlockTagProvider {
         getOrCreateTagBuilder(BlockTags.DRAGON_IMMUNE).add(UBlocks.FROSTED_OBSIDIAN, UBlocks.GOLDEN_OAK_LOG, UBlocks.GOLDEN_OAK_LEAVES);
         getOrCreateTagBuilder(BlockTags.FIRE).add(UBlocks.SPECTRAL_FIRE);
         getOrCreateTagBuilder(BlockTags.HOE_MINEABLE).add(UBlocks.HAY_BLOCK).addOptional(Unicopia.id("rice_block")).addOptional(Unicopia.id("straw_block"));
+        getOrCreateTagBuilder(BlockTags.SHOVEL_MINEABLE).add(UBlocks.WORM_BLOCK);
 
         addZapWoodset();
         addPalmWoodset();
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/loot/UBlockLootTableProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/loot/UBlockLootTableProvider.java
index 9b3b4364..fba77f65 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/loot/UBlockLootTableProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/loot/UBlockLootTableProvider.java
@@ -54,7 +54,8 @@ public class UBlockLootTableProvider extends FabricBlockLootTableProvider {
             UBlocks.WEATHER_VANE,
 
             UBlocks.ZAP_FENCE_GATE, UBlocks.ZAP_FENCE,
-            UBlocks.ZAP_LOG, UBlocks.ZAP_PLANKS, UBlocks.ZAP_STAIRS, UBlocks.ZAP_WOOD
+            UBlocks.ZAP_LOG, UBlocks.ZAP_PLANKS, UBlocks.ZAP_STAIRS, UBlocks.ZAP_WOOD,
+            UBlocks.WORM_BLOCK
         ).forEach(this::addDrop);
 
         // slabs
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java
index 942131c8..8250dd9b 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java
@@ -67,6 +67,9 @@ public class URecipeProvider extends FabricRecipeProvider {
         createCustomBedRecipe(UItems.CLOTH_BED, Either.right(ItemTags.WOOL), Either.right(ItemTags.LOGS)).offerTo(exporter);
         offerBedSheetRecipes(exporter);
 
+        // worms
+        offerReversibleCompactingRecipes(exporter, RecipeCategory.BUILDING_BLOCKS, UItems.WHEAT_WORMS, RecipeCategory.BUILDING_BLOCKS, UBlocks.WORM_BLOCK);
+
         // sunglasses
         ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, UItems.SUNGLASSES)
             .input('#', ConventionalItemTags.GLASS_BLOCKS).criterion("has_glass_block", conditionsFromTag(ConventionalItemTags.GLASS_BLOCKS))
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index 01a3726f..a83cc748 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -280,6 +280,7 @@
   "block.unicopia.golden_oak_leaves": "Golden Oak Leaves",
   "block.unicopia.golden_oak_log": "Golden Oak Log",
   "block.unicopia.mango": "Mango",
+  "block.unicopia.worm_block": "Block of Worms",
   "block.unicopia.mango_leaves": "Mango Leaves",
   "block.unicopia.mango_sapling": "Mango Sapling",
   "block.unicopia.potted_mango_sapling": "Potted Mango Sapling",
diff --git a/src/main/resources/assets/unicopia/textures/block/worm_block.png b/src/main/resources/assets/unicopia/textures/block/worm_block.png
new file mode 100644
index 0000000000000000000000000000000000000000..f27791b88df812c891fbf467a4d45e7d428e6c92
GIT binary patch
literal 7064
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
F{sHtDu_^!n

literal 0
HcmV?d00001

diff --git a/src/main/resources/data/c/tags/items/raw_insects.json b/src/main/resources/data/c/tags/items/raw_insects.json
index 4a791552..ad219e8b 100644
--- a/src/main/resources/data/c/tags/items/raw_insects.json
+++ b/src/main/resources/data/c/tags/items/raw_insects.json
@@ -2,7 +2,8 @@
   "replace": false,
   "values": [
     "minecraft:spider_eye",
+    "unicopia:butterfly",
     "unicopia:wheat_worms",
-    "unicopia:butterfly"
+    "unicopia:worm_block"
   ]
 }

From 380c35fcc6b2d66cd957842cf39a789eb3c00d85 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 18:10:12 +0000
Subject: [PATCH 09/14] Allow changelings to eat blocks (should be only humans
 here since the food rewrite)

---
 src/main/java/com/minelittlepony/unicopia/Race.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/Race.java b/src/main/java/com/minelittlepony/unicopia/Race.java
index a6ae1c58..dd3bf4d9 100644
--- a/src/main/java/com/minelittlepony/unicopia/Race.java
+++ b/src/main/java/com/minelittlepony/unicopia/Race.java
@@ -80,7 +80,7 @@ public record Race (Supplier<Composite> compositeSupplier, Availability availabi
     }
 
     public boolean hasIronGut() {
-        return !isHuman() && this != CHANGELING;
+        return !isHuman();
     }
 
     public boolean isUnset() {

From 0f681b07b32d26a2fee80e1bf2a45fcc4511eea2 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 18:11:31 +0000
Subject: [PATCH 10/14] Rebalance and beef up the insects food group to help
 changelings and bat ponies

---
 .../unicopia/diets/food_effects/insect/raw.json |  4 ++--
 .../data/unicopia/diets/races/changeling.json   | 17 ++++++++++++++++-
 .../unicopia/tags/items/groups/earth_pony.json  |  1 +
 3 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/src/main/resources/data/unicopia/diets/food_effects/insect/raw.json b/src/main/resources/data/unicopia/diets/food_effects/insect/raw.json
index 8683f240..01a176d0 100644
--- a/src/main/resources/data/unicopia/diets/food_effects/insect/raw.json
+++ b/src/main/resources/data/unicopia/diets/food_effects/insect/raw.json
@@ -1,8 +1,8 @@
 {
   "tags": [ "unicopia:food_types/raw_insect" ],
   "food_component": {
-    "hunger": 1,
-    "saturation": 0.1
+    "hunger": 6,
+    "saturation": 0.3
   },
   "ailment": {
     "effects": [
diff --git a/src/main/resources/data/unicopia/diets/races/changeling.json b/src/main/resources/data/unicopia/diets/races/changeling.json
index a60fb6bd..c20c7753 100644
--- a/src/main/resources/data/unicopia/diets/races/changeling.json
+++ b/src/main/resources/data/unicopia/diets/races/changeling.json
@@ -74,6 +74,18 @@
         ]
       }
     },
+    {
+      "tags": [
+        "unicopia:food_types/raw_insect"
+      ],
+      "food_component": {
+        "hunger": 3,
+        "saturation": 2
+      },
+      "ailment": {
+        "effects": [ ]
+      }
+    },
     {
       "tags": [
         "unicopia:food_types/cooked_fish",
@@ -98,11 +110,14 @@
       "tags": [
         "unicopia:food_types/rotten_fish",
         "unicopia:food_types/cooked_insect",
-        "unicopia:food_types/raw_insect",
         "unicopia:food_types/cooked_meat",
         "unicopia:food_types/raw_meat",
         "unicopia:food_types/rotten_meat"
       ],
+      "food_component": {
+        "hunger": 6,
+        "saturation": 9
+      },
       "ailment": {
         "effects": [
           {
diff --git a/src/main/resources/data/unicopia/tags/items/groups/earth_pony.json b/src/main/resources/data/unicopia/tags/items/groups/earth_pony.json
index 618a0532..d88b92b7 100644
--- a/src/main/resources/data/unicopia/tags/items/groups/earth_pony.json
+++ b/src/main/resources/data/unicopia/tags/items/groups/earth_pony.json
@@ -57,6 +57,7 @@
     "unicopia:crispy_hay_fries",
     "unicopia:horse_shoe_fries",
     "unicopia:wheat_worms",
+    "unicopia:worm_block",
     "unicopia:muffin",
     "unicopia:acorn",
     "unicopia:pinecone",

From 29502e7529f5ec9fa8ac4588b07ea5d638c553f8 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 18:11:50 +0000
Subject: [PATCH 11/14] Don't show diet information on blocks unaffected by the
 diet system

---
 .../unicopia/diet/PonyDiets.java              | 20 +++++++------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/diet/PonyDiets.java b/src/main/java/com/minelittlepony/unicopia/diet/PonyDiets.java
index 8e4558a9..3566bf06 100644
--- a/src/main/java/com/minelittlepony/unicopia/diet/PonyDiets.java
+++ b/src/main/java/com/minelittlepony/unicopia/diet/PonyDiets.java
@@ -77,21 +77,15 @@ public class PonyDiets implements DietView {
 
     @Override
     public void appendTooltip(ItemStack stack, @Nullable PlayerEntity user, List<Text> tooltip, TooltipContext context) {
+
         if (initEdibility(stack, user)) {
-            Pony pony = Pony.of(user);
+            if (!((ItemDuck)stack.getItem()).getOriginalFoodComponent().isEmpty() || stack.getItem().getFoodComponent() != null) {
+                Pony pony = Pony.of(user);
 
-            tooltip.add(Text.translatable("unicopia.diet.information").formatted(Formatting.DARK_PURPLE));
-            getEffects(stack, pony).appendTooltip(stack, tooltip, context);
-
-            /*for (Race race : Race.REGISTRY) {
-                var diet = diets.get(race);
-                if (diet != null) {
-                    tooltip.add(race.getDisplayName());
-                    diet.appendTooltip(stack, user, tooltip, context);
-                }
-            }*/
-
-            getDiet(pony).appendTooltip(stack, user, tooltip, context);
+                tooltip.add(Text.translatable("unicopia.diet.information").formatted(Formatting.DARK_PURPLE));
+                getEffects(stack, pony).appendTooltip(stack, tooltip, context);
+                getDiet(pony).appendTooltip(stack, user, tooltip, context);
+            }
         }
     }
 

From 54cbf2c238859d670c5f5d9f0f32d452d93f49f7 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 19:13:56 +0000
Subject: [PATCH 12/14] Add recipe to convert seashells into bonemeal

---
 .../datagen/providers/recipe/URecipeProvider.java         | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java
index 8250dd9b..ed82fb66 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/recipe/URecipeProvider.java
@@ -67,9 +67,6 @@ public class URecipeProvider extends FabricRecipeProvider {
         createCustomBedRecipe(UItems.CLOTH_BED, Either.right(ItemTags.WOOL), Either.right(ItemTags.LOGS)).offerTo(exporter);
         offerBedSheetRecipes(exporter);
 
-        // worms
-        offerReversibleCompactingRecipes(exporter, RecipeCategory.BUILDING_BLOCKS, UItems.WHEAT_WORMS, RecipeCategory.BUILDING_BLOCKS, UBlocks.WORM_BLOCK);
-
         // sunglasses
         ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, UItems.SUNGLASSES)
             .input('#', ConventionalItemTags.GLASS_BLOCKS).criterion("has_glass_block", conditionsFromTag(ConventionalItemTags.GLASS_BLOCKS))
@@ -452,6 +449,9 @@ public class URecipeProvider extends FabricRecipeProvider {
             .pattern("---")
             .offerTo(exporter);
 
+        // worms
+        offerReversibleCompactingRecipes(exporter, RecipeCategory.BUILDING_BLOCKS, UItems.WHEAT_WORMS, RecipeCategory.BUILDING_BLOCKS, UBlocks.WORM_BLOCK);
+
         // utility
         ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, Items.DIRT)
             .input('*', UItems.WHEAT_WORMS).criterion("has_wheat_worms", conditionsFromItem(UItems.WHEAT_WORMS))
@@ -460,6 +460,8 @@ public class URecipeProvider extends FabricRecipeProvider {
             .pattern("#*")
             .offerTo(exporter, convertBetween(Items.DIRT, UItems.WHEAT_WORMS));
 
+        offerShapelessRecipe(exporter, Items.BONE_MEAL, UTags.SHELLS, "bonemeal", 3);
+
         offer2x2CompactingRecipe(exporter, RecipeCategory.BUILDING_BLOCKS, Items.COBBLESTONE, UItems.ROCK);
         offerReversibleCompactingRecipesWithReverseRecipeGroup(exporter, RecipeCategory.MISC, UItems.PEBBLES, RecipeCategory.BUILDING_BLOCKS, Blocks.GRAVEL, convertBetween(UItems.PEBBLES, Blocks.GRAVEL), "pebbles");
         offerShapelessRecipe(exporter, UItems.PEBBLES, Blocks.SUSPICIOUS_GRAVEL, "pebbles", 9);

From 9e329ac2f92d1b8cbacbd297eb389a47b4c5a820 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Mon, 25 Mar 2024 19:57:21 +0000
Subject: [PATCH 13/14] #314 Reduce camera motion and apply the client setting
 for fov scale to the flight camera

---
 .../unicopia/entity/player/PlayerCamera.java  | 40 ++++++++++---------
 .../mixin/client/MixinGameRenderer.java       |  2 +-
 2 files changed, 22 insertions(+), 20 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerCamera.java b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerCamera.java
index 26c6d4c7..61effece 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerCamera.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerCamera.java
@@ -7,6 +7,7 @@ import com.minelittlepony.unicopia.ability.magic.SpellPredicate;
 import com.minelittlepony.unicopia.ability.magic.spell.AbstractDisguiseSpell;
 import com.minelittlepony.unicopia.client.render.spell.DarkVortexSpellRenderer;
 
+import net.minecraft.client.MinecraftClient;
 import net.minecraft.util.math.Vec3d;
 
 public class PlayerCamera extends MotionCompositor {
@@ -18,30 +19,28 @@ public class PlayerCamera extends MotionCompositor {
     }
 
     public float calculateRoll() {
+        return player.getInterpolator().interpolate("roll", (float)applyModifiers(-getMotionRoll()), 15);
+    }
 
-        double roll = 0;
+    public float calculateFirstPersonRoll() {
+        return player.getInterpolator().interpolate("roll_fp", (float)applyModifiers(-getMotionRoll() * getFovScale() * 0.25F), 25);
+    }
 
-        if (player.getMotion().isFlying()) {
-            Vec3d vel = player.asEntity().getVelocity();
-
-            roll -= calculateRoll(player.asEntity(), vel.x, vel.y, vel.z);
-        }
-
-        if (player.getPhysics().isGravityNegative()) {
-           roll *= -1;
-           roll += 180;
-        }
-
-        if (player.asEntity().age > 10) {
-            roll = player.getInterpolator().interpolate("roll", (float)roll, 15);
+    private double getMotionRoll() {
+        if (!player.getMotion().isFlying() || player.asEntity().hasVehicle() || player.asEntity().isOnGround()) {
+            return 0;
         }
 
+        Vec3d vel = player.asEntity().getVelocity();
+        return calculateRoll(player.asEntity(), vel.x, vel.y, vel.z);
+    }
 
+    private double applyModifiers(double motionRoll) {
         if (player.getAcrobatics().isFloppy()) {
-            roll += 90;
+            motionRoll += 90;
         }
 
-        return (float)roll;
+        return player.getPhysics().isGravityNegative() ? 180 - motionRoll : motionRoll;
     }
 
     public float calculatePitch(float pitch) {
@@ -61,13 +60,16 @@ public class PlayerCamera extends MotionCompositor {
     }
 
     public double calculateFieldOfView(double fov) {
-        fov += player.getMagicalReserves().getExertion().get() / 5F;
-        fov += getEnergyAddition();
+        fov += (player.getMagicalReserves().getExertion().get() / 5F) * getFovScale();
+        fov += getEnergyAddition() * getFovScale();
         fov += DarkVortexSpellRenderer.getCameraDistortion() * 2.5F;
-
         return fov;
     }
 
+    private float getFovScale() {
+        return MinecraftClient.getInstance().options.getFovEffectScale().getValue().floatValue();
+    }
+
     protected float getEnergyAddition() {
         int maxE = (int)Math.floor(player.getMagicalReserves().getEnergy().get() * 100);
 
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinGameRenderer.java b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinGameRenderer.java
index d71373fb..2fe4abe2 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinGameRenderer.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinGameRenderer.java
@@ -36,7 +36,7 @@ abstract class MixinGameRenderer implements AutoCloseable, SynchronousResourceRe
     @Inject(method = "renderWorld(FJLnet/minecraft/client/util/math/MatrixStack;)V",
             at = @At("HEAD"))
     private void beforeRenderWorld(float tickDelta, long limitTime, MatrixStack matrices, CallbackInfo info) {
-        UnicopiaClient.getCamera().ifPresent(c -> matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(c.calculateRoll())));
+        UnicopiaClient.getCamera().ifPresent(c -> matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(c.calculateFirstPersonRoll())));
         BatEyesApplicator.INSTANCE.enable();
     }
 

From 2dd7fbbb5530961f5bd25f2df72bfab81d3639bf Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Tue, 26 Mar 2024 00:04:00 +0000
Subject: [PATCH 14/14] Fix a whole bunch of gravity related issues

---
 .../unicopia/client/UnicopiaClient.java       | 18 +++++
 .../unicopia/entity/EntityPhysics.java        | 10 +++
 .../unicopia/entity/duck/RotatedView.java     |  3 +-
 .../unicopia/entity/player/PlayerPhysics.java | 17 -----
 .../unicopia/mixin/MixinLivingEntity.java     | 10 ---
 .../unicopia/mixin/MixinMobEntity.java        | 13 ----
 .../unicopia/mixin/MixinPlayerEntity.java     |  8 --
 .../unicopia/mixin/MixinWorld.java            | 30 --------
 .../mixin/client/MixinKeyboardInput.java      |  4 +-
 .../mixin/{ => gravity}/MixinBrain.java       |  4 +-
 .../mixin/gravity/MixinClientWorld.java       | 30 ++++++++
 .../unicopia/mixin/gravity/MixinEntity.java   | 76 +++++++++++++++++++
 .../mixin/gravity/MixinLivingEntity.java      | 22 ++++++
 .../mixin/gravity/MixinMobEntity.java         | 28 +++++++
 .../mixin/gravity/MixinServerWorld.java       | 29 +++++++
 .../mixin/gravity/MixinSoundSource.java       | 17 +++++
 .../unicopia/mixin/gravity/MixinWorld.java    | 43 +++++++++++
 .../mixin/{ => gravity}/MixinWorldChunk.java  |  2 +-
 src/main/resources/unicopia.mixin.json        | 13 +++-
 19 files changed, 290 insertions(+), 87 deletions(-)
 rename src/main/java/com/minelittlepony/unicopia/mixin/{ => gravity}/MixinBrain.java (87%)
 create mode 100644 src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinClientWorld.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinEntity.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinLivingEntity.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinMobEntity.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinServerWorld.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinSoundSource.java
 create mode 100644 src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinWorld.java
 rename src/main/java/com/minelittlepony/unicopia/mixin/{ => gravity}/MixinWorldChunk.java (96%)

diff --git a/src/main/java/com/minelittlepony/unicopia/client/UnicopiaClient.java b/src/main/java/com/minelittlepony/unicopia/client/UnicopiaClient.java
index e7face1d..c8d9bcff 100644
--- a/src/main/java/com/minelittlepony/unicopia/client/UnicopiaClient.java
+++ b/src/main/java/com/minelittlepony/unicopia/client/UnicopiaClient.java
@@ -3,6 +3,7 @@ package com.minelittlepony.unicopia.client;
 import java.util.Optional;
 
 import org.jetbrains.annotations.Nullable;
+import org.joml.Vector3f;
 
 import com.minelittlepony.common.client.gui.element.Button;
 import com.minelittlepony.common.event.ScreenInitCallback;
@@ -33,11 +34,14 @@ import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.OpenToLanScreen;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.screen.ingame.HandledScreens;
+import net.minecraft.client.render.Camera;
 import net.minecraft.client.world.ClientWorld;
 import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.resource.ResourceType;
 import net.minecraft.text.Text;
 import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
 
 public class UnicopiaClient implements ClientModInitializer {
 
@@ -66,6 +70,20 @@ public class UnicopiaClient implements ClientModInitializer {
         return Optional.empty();
     }
 
+
+    public static Vec3d getAdjustedSoundPosition(Vec3d pos) {
+        PlayerCamera cam = getCamera().orElse(null);
+        if (cam == null) {
+            return pos;
+        }
+        Camera camera = MinecraftClient.getInstance().gameRenderer.getCamera();
+
+        Vector3f rotated = pos.subtract(camera.getPos()).toVector3f();
+        rotated = rotated.rotateAxis(cam.calculateRoll() * MathHelper.RADIANS_PER_DEGREE, 0, 1, 0);
+
+        return new Vec3d(rotated).add(camera.getPos());
+    }
+
     public static Race getPreferredRace() {
         if (!Unicopia.getConfig().ignoreMineLP.get()
                 && MinecraftClient.getInstance().player != null) {
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java b/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java
index 1529989b..538e7e4a 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/EntityPhysics.java
@@ -8,7 +8,9 @@ import net.minecraft.block.BlockRenderType;
 import net.minecraft.block.BlockState;
 import net.minecraft.block.FenceGateBlock;
 import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityPose;
 import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.MovementType;
 import net.minecraft.entity.data.TrackedData;
 import net.minecraft.entity.mob.MobEntity;
 import net.minecraft.nbt.NbtCompound;
@@ -35,6 +37,14 @@ public class EntityPhysics<T extends Entity> implements Physics, Copyable<Entity
     @Override
     public void tick() {
         if (isGravityNegative()) {
+            if (isGravityNegative() && !entity.isSneaking() && entity.isInSneakingPose()) {
+                float currentHeight = entity.getDimensions(entity.getPose()).height;
+                float sneakingHeight = entity.getDimensions(EntityPose.STANDING).height;
+
+                entity.move(MovementType.SELF, new Vec3d(0, -(currentHeight - sneakingHeight), 0));
+                entity.setPose(EntityPose.STANDING);
+            }
+
             if (entity.getY() > entity.getWorld().getHeight() + 64) {
                 entity.damage(entity.getDamageSources().outOfWorld(), 4.0F);
             }
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/duck/RotatedView.java b/src/main/java/com/minelittlepony/unicopia/entity/duck/RotatedView.java
index e5c60d40..0bc5855b 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/duck/RotatedView.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/duck/RotatedView.java
@@ -39,7 +39,8 @@ public interface RotatedView {
             if (!hasTransform() || rotations.isEmpty()) {
                 return y;
             }
-            return y - ((y - rotations.peek()) * 2);
+
+            return (rotations.peek() * 2) - y;
         }
     }
 
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java
index 35116ead..f6c82b63 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/player/PlayerPhysics.java
@@ -32,7 +32,6 @@ import com.minelittlepony.unicopia.util.*;
 import net.fabricmc.fabric.api.tag.convention.v1.ConventionalBlockTags;
 import net.minecraft.block.*;
 import net.minecraft.enchantment.EnchantmentHelper;
-import net.minecraft.entity.EntityPose;
 import net.minecraft.entity.EntityType;
 import net.minecraft.entity.EquipmentSlot;
 import net.minecraft.entity.LightningEntity;
@@ -253,18 +252,6 @@ public class PlayerPhysics extends EntityPhysics<PlayerEntity> implements Tickab
 
         final MutableVector velocity = new MutableVector(entity.getVelocity());
 
-        if (isGravityNegative()) {
-            velocity.y *= -1;
-        }
-
-        if (isGravityNegative() && !entity.isSneaking() && entity.isInSneakingPose()) {
-            float currentHeight = entity.getDimensions(entity.getPose()).height;
-            float sneakingHeight = entity.getDimensions(EntityPose.STANDING).height;
-
-            entity.setPos(entity.getX(), entity.getY() + currentHeight - sneakingHeight, entity.getZ());
-            entity.setPose(EntityPose.STANDING);
-        }
-
         FlightType type = recalculateFlightType();
 
         boolean typeChanged = type != lastFlightType;
@@ -396,10 +383,6 @@ public class PlayerPhysics extends EntityPhysics<PlayerEntity> implements Tickab
             velocity.z /= heavyness;
         }
 
-        if (isGravityNegative()) {
-            velocity.y *= -1;
-        }
-
         entity.setVelocity(velocity.toImmutable());
 
         if (isFlying() && !entity.isFallFlying() && !pony.getAcrobatics().isHanging() && pony.isClient()) {
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java
index f71ef863..b1e811b0 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java
@@ -5,9 +5,7 @@ import java.util.Optional;
 import org.spongepowered.asm.mixin.*;
 import org.spongepowered.asm.mixin.gen.Accessor;
 import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Constant;
 import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.ModifyConstant;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
@@ -178,14 +176,6 @@ abstract class MixinLivingEntity extends Entity implements LivingEntityDuck, Equ
         }
     }
 
-    @ModifyConstant(method = "travel(Lnet/minecraft/util/math/Vec3d;)V", constant = {
-            @Constant(doubleValue = 0.08D),
-            @Constant(doubleValue = 0.01D)
-    })
-    private double modifyGravity(double initial) {
-        return get().getPhysics().calcGravity(initial);
-    }
-
     @Override
     public void updateItemUsage(Hand hand, ItemStack stack, int time) {
         activeItemStack = stack;
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinMobEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinMobEntity.java
index d836f0fc..d9200a75 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinMobEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinMobEntity.java
@@ -9,7 +9,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
 import com.minelittlepony.unicopia.entity.*;
-import com.minelittlepony.unicopia.entity.duck.RotatedView;
 import com.minelittlepony.unicopia.item.enchantment.WantItNeedItEnchantment;
 
 import net.minecraft.entity.EntityType;
@@ -33,18 +32,6 @@ abstract class MixinMobEntity extends LivingEntity implements Equine.Container<C
         get().initAi(goalSelector, targetSelector);
     }
 
-    @Inject(method = "tickNewAi", at = @At("HEAD"))
-    public void beforeTickAi(CallbackInfo into) {
-        if (get().getPhysics().isGravityNegative()) {
-            ((RotatedView)getWorld()).pushRotation((int)getY());
-        }
-    }
-
-    @Inject(method = "tickNewAi", at = @At("RETURN"))
-    public void afterTickAi(CallbackInfo into) {
-        ((RotatedView)getWorld()).popRotation();
-    }
-
     @Inject(method = "prefersNewEquipment(Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ItemStack;)Z",
             at = @At("HEAD"), cancellable = true)
     private void onPrefersNewEquipment(ItemStack newStack, ItemStack oldStack, CallbackInfoReturnable<Boolean> info) {
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinPlayerEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinPlayerEntity.java
index d61ad769..b67432fc 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinPlayerEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinPlayerEntity.java
@@ -97,14 +97,6 @@ abstract class MixinPlayerEntity extends LivingEntity implements Equine.Containe
         get().getMotion().getDimensions().calculateActiveEyeHeight(dimensions).ifPresent(info::setReturnValue);
     }
 
-    /*
-    @Inject(method = "getDimensions(Lnet/minecraft/entity/EntityPose;)Lnet/minecraft/entity/EntityDimensions;",
-            at = @At("RETURN"),
-            cancellable = true)
-    private void onGetDimensions(EntityPose pose, CallbackInfoReturnable<EntityDimensions> info) {
-        get().getMotion().getDimensions().calculateDimensions().ifPresent(info::setReturnValue);
-    }*/
-
     @Redirect(method = "getDimensions(Lnet/minecraft/entity/EntityPose;)Lnet/minecraft/entity/EntityDimensions;",
             at = @At(
                 value = "INVOKE",
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java
index 04db071b..99ca9150 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java
@@ -1,19 +1,13 @@
 package com.minelittlepony.unicopia.mixin;
 
-import java.util.Stack;
 import java.util.function.Supplier;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.ModifyVariable;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-
 import com.minelittlepony.unicopia.entity.duck.RotatedView;
 import com.minelittlepony.unicopia.server.world.BlockDestructionManager;
-import net.minecraft.block.BlockState;
 import net.minecraft.entity.Entity;
-import net.minecraft.util.math.BlockPos;
 import net.minecraft.world.World;
 import net.minecraft.world.WorldAccess;
 
@@ -22,20 +16,8 @@ abstract class MixinWorld implements WorldAccess, BlockDestructionManager.Source
 
     private final Supplier<BlockDestructionManager> destructions = BlockDestructionManager.create((World)(Object)this);
 
-    private int recurseCount = 0;
-    private final Stack<Integer> rotations = new Stack<>();
     private boolean mirrorEntityStatuses;
 
-    @Override
-    public Stack<Integer> getRotations() {
-        return rotations;
-    }
-
-    @Override
-    public boolean hasTransform() {
-        return recurseCount <= 0;
-    }
-
     @Override
     public void setMirrorEntityStatuses(boolean enable) {
         mirrorEntityStatuses = enable;
@@ -52,17 +34,5 @@ abstract class MixinWorld implements WorldAccess, BlockDestructionManager.Source
             entity.handleStatus(status);
         }
     }
-
-    @ModifyVariable(method = "setBlockState(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;II)Z", at = @At("HEAD"))
-    private BlockPos modifyBlockPos(BlockPos pos) {
-        pos = applyRotation(pos);
-        recurseCount = Math.max(0, recurseCount) + 1;
-        return pos;
-    }
-
-    @Inject(method = "setBlockState(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;II)Z", at = @At("RETURN"))
-    public void onSetBlockState(BlockPos pos, BlockState state, int flags, int maxUpdateDepth, CallbackInfoReturnable<Boolean> info) {
-        recurseCount = Math.max(0, recurseCount - 1);
-    }
 }
 
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinKeyboardInput.java b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinKeyboardInput.java
index 7bc83f17..55c8a70b 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinKeyboardInput.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/client/MixinKeyboardInput.java
@@ -28,11 +28,11 @@ abstract class MixinKeyboardInput extends Input {
 
                 movementSideways = -movementSideways;
 
-                if (player.asEntity().getAbilities().flying && !player.getPhysics().isFlying()) {
+                /*if (player.asEntity().getAbilities().flying && !player.getPhysics().isFlying()) {
                     tmp = jumping;
                     jumping = sneaking;
                     sneaking = tmp;
-                }
+                }*/
             }
 
             if (EffectUtils.getAmplifier(MinecraftClient.getInstance().player, UEffects.PARALYSIS) > 1) {
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinBrain.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinBrain.java
similarity index 87%
rename from src/main/java/com/minelittlepony/unicopia/mixin/MixinBrain.java
rename to src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinBrain.java
index 7a7a7737..4e794c6a 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinBrain.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinBrain.java
@@ -1,4 +1,4 @@
-package com.minelittlepony.unicopia.mixin;
+package com.minelittlepony.unicopia.mixin.gravity;
 
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
@@ -21,7 +21,7 @@ abstract class MixinBrain<E extends LivingEntity> {
         Equine<?> eq = Equine.of(entity).orElse(null);
 
         if (eq instanceof Living<?> && eq.getPhysics().isGravityNegative()) {
-            ((RotatedView)world).pushRotation((int)entity.getY());
+            ((RotatedView)world).pushRotation((int)(entity.getY() + entity.getHeight() * 0.5F));
         }
     }
 
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinClientWorld.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinClientWorld.java
new file mode 100644
index 00000000..98701eec
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinClientWorld.java
@@ -0,0 +1,30 @@
+package com.minelittlepony.unicopia.mixin.gravity;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import com.minelittlepony.unicopia.entity.Equine;
+import com.minelittlepony.unicopia.entity.duck.RotatedView;
+
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+
+@Mixin(ClientWorld.class)
+abstract class MixinClientWorld implements RotatedView {
+
+    @Inject(method = "tickEntity", at = @At("HEAD"))
+    private void beforeTickEntity(Entity entity, CallbackInfo info) {
+        if (entity instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+           // pushRotation((int)(entity.getY() + entity.getHeight() * 0.5F));
+        }
+    }
+
+    @Inject(method = "tickEntity", at = @At("RETURN"))
+    private void afterTickEntity(Entity entity, CallbackInfo info) {
+        if (entity instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+           // popRotation();
+        }
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinEntity.java
new file mode 100644
index 00000000..c0725d94
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinEntity.java
@@ -0,0 +1,76 @@
+package com.minelittlepony.unicopia.mixin.gravity;
+
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.At.Shift;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import com.minelittlepony.unicopia.entity.Equine;
+
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.MovementType;
+import net.minecraft.util.math.Vec3d;
+
+@Mixin(value = Entity.class, priority = 29000)
+abstract class MixinEntity {
+
+    // we invert y when moving
+    @ModifyVariable(method = "move", at = @At("HEAD"), argsOnly = true)
+    private Vec3d modifyMovement(Vec3d movement) {
+        if (this instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+            return movement.multiply(1, -1, 1);
+        }
+        return movement;
+    }
+
+    // fix on ground check
+    @Inject(method = "move", at = @At(value = "FIELD", target = "net/minecraft/entity/Entity.groundCollision:Z", shift = Shift.AFTER, ordinal = 0))
+    private void onUpdateOnGroundFlag(MovementType movementType, Vec3d movement, CallbackInfo info) {
+        if (this instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+            eq.get().asEntity().groundCollision = eq.get().asEntity().verticalCollision && movement.y > 0.0;
+        }
+    }
+
+    // invert jumping velocity so we can jump
+    @Inject(method = "getJumpVelocityMultiplier", at = @At("RETURN"), cancellable = true)
+    private void onGetJumpVelocityMultiplier(CallbackInfoReturnable<Float> info) {
+        if (this instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+            info.setReturnValue(-info.getReturnValue());
+        }
+    }
+
+    // invert offsets so it can properly find the block we're walking on
+    @ModifyVariable(method = "getPosWithYOffset", at = @At("HEAD"), argsOnly = true)
+    private float onGetPosWithYOffset(float offset) {
+        if (this instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+            return -(eq.get().asEntity().getHeight() + offset);
+        }
+        return offset;
+    }
+
+    // fix sprinting particles
+    @Inject(method = "spawnSprintingParticles", at = @At("HEAD"), cancellable = true)
+    protected void spawnSprintingParticles(CallbackInfo info) {
+        if (this instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+            eq.get().getPhysics().spawnSprintingParticles();
+            info.cancel();
+        }
+    }
+
+    // invert check for walking up a step
+    @ModifyVariable(
+            method = "adjustMovementForCollisions(Lnet/minecraft/entity/Entity;Lnet/minecraft/util/math/Vec3d;Lnet/minecraft/util/math/Box;Lnet/minecraft/world/World;Ljava/util/List;)Lnet/minecraft/util/math/Vec3d;",
+            at = @At("HEAD"),
+            argsOnly = true)
+
+    private static Vec3d modifyMovementForStepheight(Vec3d movement, @Nullable Entity entity) {
+        if (entity != null && movement.getY() == entity.getStepHeight()) {
+            return movement.multiply(1, -1, 1);
+        }
+        return movement;
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinLivingEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinLivingEntity.java
new file mode 100644
index 00000000..2d9dfe6c
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinLivingEntity.java
@@ -0,0 +1,22 @@
+package com.minelittlepony.unicopia.mixin.gravity;
+
+import org.spongepowered.asm.mixin.*;
+import org.spongepowered.asm.mixin.injection.Constant;
+import org.spongepowered.asm.mixin.injection.ModifyConstant;
+import com.minelittlepony.unicopia.entity.*;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.LivingEntity;
+
+@Mixin(LivingEntity.class)
+abstract class MixinLivingEntity extends Entity implements Equine.Container<Living<?>> {
+
+    private MixinLivingEntity() { super(null, null); }
+
+    @ModifyConstant(method = "travel(Lnet/minecraft/util/math/Vec3d;)V", constant = {
+            @Constant(doubleValue = 0.08D),
+            @Constant(doubleValue = 0.01D)
+    })
+    private double modifyGravity(double initial) {
+        return Math.abs(get().getPhysics().calcGravity(initial));
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinMobEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinMobEntity.java
new file mode 100644
index 00000000..e72ec42a
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinMobEntity.java
@@ -0,0 +1,28 @@
+package com.minelittlepony.unicopia.mixin.gravity;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import com.minelittlepony.unicopia.entity.*;
+import com.minelittlepony.unicopia.entity.duck.RotatedView;
+
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.mob.MobEntity;
+
+@Mixin(MobEntity.class)
+abstract class MixinMobEntity extends LivingEntity implements Equine.Container<Creature> {
+    private MixinMobEntity() { super(null, null); }
+
+    @Inject(method = "tickNewAi", at = @At("HEAD"))
+    public void beforeTickAi(CallbackInfo into) {
+        if (get().getPhysics().isGravityNegative()) {
+            ((RotatedView)getWorld()).pushRotation((int)(getY() + getHeight() * 0.5F));
+        }
+    }
+
+    @Inject(method = "tickNewAi", at = @At("RETURN"))
+    public void afterTickAi(CallbackInfo into) {
+        ((RotatedView)getWorld()).popRotation();
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinServerWorld.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinServerWorld.java
new file mode 100644
index 00000000..d43d1570
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinServerWorld.java
@@ -0,0 +1,29 @@
+package com.minelittlepony.unicopia.mixin.gravity;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import com.minelittlepony.unicopia.entity.Equine;
+import com.minelittlepony.unicopia.entity.duck.RotatedView;
+import net.minecraft.entity.Entity;
+import net.minecraft.server.world.ServerWorld;
+
+@Mixin(ServerWorld.class)
+abstract class MixinServerWorld implements RotatedView {
+
+    @Inject(method = "tickEntity", at = @At("HEAD"))
+    private void beforeTickEntity(Entity entity, CallbackInfo info) {
+        if (entity instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+           // pushRotation((int)(entity.getY() + entity.getHeight() * 0.5F));
+        }
+    }
+
+    @Inject(method = "tickEntity", at = @At("RETURN"))
+    private void afterTickEntity(Entity entity, CallbackInfo info) {
+        if (entity instanceof Equine.Container eq && eq.get().getPhysics().isGravityNegative()) {
+           // popRotation();
+        }
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinSoundSource.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinSoundSource.java
new file mode 100644
index 00000000..7ffbad9f
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinSoundSource.java
@@ -0,0 +1,17 @@
+package com.minelittlepony.unicopia.mixin.gravity;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+
+import com.minelittlepony.unicopia.client.UnicopiaClient;
+import net.minecraft.client.sound.Source;
+import net.minecraft.util.math.Vec3d;
+
+@Mixin(Source.class)
+abstract class MixinSoundSource {
+    @ModifyVariable(method = "setPosition", at = @At("HEAD"), argsOnly = true)
+    private Vec3d modifyPosition(Vec3d pos) {
+        return UnicopiaClient.getAdjustedSoundPosition(pos);
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinWorld.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinWorld.java
new file mode 100644
index 00000000..71428388
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinWorld.java
@@ -0,0 +1,43 @@
+package com.minelittlepony.unicopia.mixin.gravity;
+
+import java.util.Stack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import com.minelittlepony.unicopia.entity.duck.RotatedView;
+import net.minecraft.block.BlockState;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import net.minecraft.world.WorldAccess;
+
+@Mixin(World.class)
+abstract class MixinWorld implements WorldAccess, RotatedView {
+
+    private int recurseCount = 0;
+    private final Stack<Integer> rotations = new Stack<>();
+
+    @Override
+    public Stack<Integer> getRotations() {
+        return rotations;
+    }
+
+    @Override
+    public boolean hasTransform() {
+        return recurseCount <= 0;
+    }
+
+    @ModifyVariable(method = "setBlockState(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;II)Z", at = @At("HEAD"))
+    private BlockPos modifyBlockPos(BlockPos pos) {
+        pos = applyRotation(pos);
+        recurseCount = Math.max(0, recurseCount) + 1;
+        return pos;
+    }
+
+    @Inject(method = "setBlockState(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;II)Z", at = @At("RETURN"))
+    public void onSetBlockState(BlockPos pos, BlockState state, int flags, int maxUpdateDepth, CallbackInfoReturnable<Boolean> info) {
+        recurseCount = Math.max(0, recurseCount - 1);
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorldChunk.java b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinWorldChunk.java
similarity index 96%
rename from src/main/java/com/minelittlepony/unicopia/mixin/MixinWorldChunk.java
rename to src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinWorldChunk.java
index 97f137d8..4a3c2ced 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorldChunk.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/gravity/MixinWorldChunk.java
@@ -1,4 +1,4 @@
-package com.minelittlepony.unicopia.mixin;
+package com.minelittlepony.unicopia.mixin.gravity;
 
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
diff --git a/src/main/resources/unicopia.mixin.json b/src/main/resources/unicopia.mixin.json
index d59bf63e..0d34a3bf 100644
--- a/src/main/resources/unicopia.mixin.json
+++ b/src/main/resources/unicopia.mixin.json
@@ -13,7 +13,6 @@
     "MixinBlockEntityType",
     "MixinBlockItem",
     "MixinBoatEntity",
-    "MixinBrain",
     "MixinChunkBlockLightProvider",
     "MutableBlockLightStorage",
     "MixinDamageSource",
@@ -53,8 +52,14 @@
     "MixinVanillaBiomeParameters",
     "MixinWardenEntity",
     "MixinWorld",
-    "MixinWorldChunk",
     "PointOfInterestTypesAccessor",
+    "gravity.MixinBrain",
+    "gravity.MixinEntity",
+    "gravity.MixinLivingEntity",
+    "gravity.MixinMobEntity",
+    "gravity.MixinWorld",
+    "gravity.MixinServerWorld",
+    "gravity.MixinWorldChunk",
     "trinkets.MixinTrinketSurvivalSlot",
     "trinkets.MixinTrinketItem",
     "trinkets.MixinTrinketInventory",
@@ -88,7 +93,9 @@
     "client.MixinWorldRenderer",
     "client.sodium.MixinSodiumWorldRenderer",
     "client.minelp.MixinPonyPosture",
-    "trinkets.MixinTrinketCreativeSlot"
+    "trinkets.MixinTrinketCreativeSlot",
+    "gravity.MixinClientWorld",
+    "gravity.MixinSoundSource"
   ],
   "injectors": {
     "defaultRequire": 1