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() {
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/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/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/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/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/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/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/datagen/providers/UAdvancementsProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
index b815f246..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;
@@ -54,31 +53,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, "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")
@@ -149,13 +154,14 @@ 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");
+                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 -> {
+             .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");
              });
@@ -169,15 +175,14 @@ 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");
         });
     }
 
     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 +194,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)
@@ -215,7 +221,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/datagen/providers/UBlockStateModelGenerator.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UBlockStateModelGenerator.java
index 2b86e3d2..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);
@@ -381,9 +382,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);
     }
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..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
@@ -449,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))
@@ -457,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);
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);
+            }
         }
     }
 
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/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/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/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..9200384f 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,13 +16,17 @@ 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)
+    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)
@@ -74,6 +77,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 +97,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 +114,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/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/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/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();
     }
 
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/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/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
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index ccba8c05..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",
@@ -355,6 +356,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",
@@ -1598,6 +1600,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",
@@ -1618,6 +1622,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",
 
@@ -1647,11 +1655,15 @@
   "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",
 
-  "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!",
@@ -1668,6 +1680,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",
@@ -1675,8 +1695,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,11 +1728,13 @@
   "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!",
+  "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...",
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/block/worm_block.png b/src/main/resources/assets/unicopia/textures/block/worm_block.png
new file mode 100644
index 00000000..f27791b8
Binary files /dev/null and b/src/main/resources/assets/unicopia/textures/block/worm_block.png differ
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 00000000..a0ea4822
Binary files /dev/null and b/src/main/resources/assets/unicopia/textures/particle/footprint.png differ
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"
   ]
 }
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",
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