From ff4770314046bd34f1ea5a4e80d1c5e2fd65f4c0 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Wed, 27 Mar 2024 19:43:55 +0000
Subject: [PATCH 01/11] Update blockus

---
 BlockusAddon | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/BlockusAddon b/BlockusAddon
index 7170edad..2e285380 160000
--- a/BlockusAddon
+++ b/BlockusAddon
@@ -1 +1 @@
-Subproject commit 7170edad67426756e2383bd9464a8615e9bb4b3a
+Subproject commit 2e285380cfa55da1b858c38833f05a56666c219f

From 93d11531d68aeaf91355643901024cf4c3ef9495 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Wed, 27 Mar 2024 19:44:12 +0000
Subject: [PATCH 02/11] Use newer loom features for datagen

---
 build.gradle | 23 +++++------------------
 1 file changed, 5 insertions(+), 18 deletions(-)

diff --git a/build.gradle b/build.gradle
index 285777fc..69922b16 100644
--- a/build.gradle
+++ b/build.gradle
@@ -28,18 +28,13 @@ archivesBaseName = project.name
 loom {
     mixin.defaultRefmapName = 'unicopia.mixin.refmap.json'
     accessWidenerPath = file('src/main/resources/unicopia.aw')
-    runs {
-        datagen {
-            server()
-            name "Data Generation"
-            vmArg "-Dfabric-api.datagen"
-            vmArg "-Dfabric-api.datagen.modid=unicopia"
-            vmArg "-Dfabric-api.datagen.output-dir=${file("src/main/generated")}"
-            runDir "build/datagen"
-        }
+}
+
+fabricApi {
+    configureDataGeneration {
+      modId = 'unicopia'
     }
 }
-//assemble.dependsOn(runDatagen)
 
 reckon {
     scopeFromProp()
@@ -108,14 +103,6 @@ dependencies {
     }
 }
 
-sourceSets {
-  main {
-    resources {
-      srcDirs += [ "src/main/generated" ]
-    }
-  }
-}
-
 processResources {
     inputs.property "version", project.version.toString()
 

From fbe444b56cd52e30383d50a92629f41c7e8c89a0 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 12:08:06 +0000
Subject: [PATCH 03/11] Fix janky hot air balloon physics

---
 .../client/render/WorldRenderDelegate.java    |   2 -
 .../unicopia/entity/Living.java               | 127 +---------
 .../unicopia/entity/Transportation.java       | 141 +++++++++++
 .../collision/MultiBoundingBoxEntity.java     |  26 +-
 .../unicopia/entity/mob/AirBalloonEntity.java | 232 +++++++++---------
 .../unicopia/mixin/MixinEntity.java           |  18 ++
 6 files changed, 313 insertions(+), 233 deletions(-)
 create mode 100644 src/main/java/com/minelittlepony/unicopia/entity/Transportation.java

diff --git a/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java b/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java
index 17594ba0..f839fef1 100644
--- a/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java
+++ b/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java
@@ -167,8 +167,6 @@ public class WorldRenderDelegate {
             return true;
         }
 
-        pony.updateSupportingEntity();
-
         matrices.push();
 
         Entity owner = pony.asEntity();
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Living.java b/src/main/java/com/minelittlepony/unicopia/entity/Living.java
index 84dce958..86ea4261 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/Living.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/Living.java
@@ -20,7 +20,6 @@ import com.minelittlepony.unicopia.advancement.UCriteria;
 import com.minelittlepony.unicopia.compat.trinkets.TrinketsDelegate;
 import com.minelittlepony.unicopia.entity.behaviour.EntityAppearance;
 import com.minelittlepony.unicopia.entity.behaviour.Guest;
-import com.minelittlepony.unicopia.entity.collision.MultiBoundingBoxEntity;
 import com.minelittlepony.unicopia.entity.damage.MagicalDamageSource;
 import com.minelittlepony.unicopia.entity.duck.LivingEntityDuck;
 import com.minelittlepony.unicopia.entity.effect.CorruptInfluenceStatusEffect;
@@ -67,7 +66,6 @@ import net.minecraft.sound.SoundCategory;
 import net.minecraft.util.Hand;
 import net.minecraft.util.hit.BlockHitResult;
 import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.Box;
 import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.MathHelper;
 import net.minecraft.util.math.Vec3d;
@@ -88,14 +86,6 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
 
     private boolean invisible = false;
 
-    @Nullable
-    private Entity supportingEntity;
-
-    @Nullable
-    private Vec3d supportPositionOffset;
-    private int ticksOutsideVehicle;
-    private int ticksInVehicle;
-
     @Nullable
     private Caster<?> attacker;
     @Nullable
@@ -109,6 +99,8 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
 
     private final Enchantments enchants = addTicker(new Enchantments(this));
     private final ItemTracker armour = addTicker(new ItemTracker(this));
+    //private final Transportation<T> transportation = new Transportation<>(this);
+    private final Transportation<T> transportation = new Transportation<>(this);
 
     protected Living(T entity, TrackedData<NbtCompound> effect) {
         this.entity = entity;
@@ -171,6 +163,10 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
         return armour;
     }
 
+    public Transportation<T> getTransportation() {
+        return transportation;
+    }
+
     @Override
     public final T asEntity() {
         return entity;
@@ -202,73 +198,6 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
         return vehicle != null && getCarrierId().filter(vehicle.getUuid()::equals).isPresent();
     }
 
-    public boolean setSupportingEntity(@Nullable Entity supportingEntity) {
-        this.supportingEntity = supportingEntity;
-        if (supportingEntity != null) {
-            ticksOutsideVehicle = 0;
-        }
-        return true;
-    }
-
-    @Nullable
-    public Entity getSupportingEntity() {
-        return supportingEntity;
-    }
-
-    public int getTicksInVehicle() {
-        return ticksInVehicle;
-    }
-
-    public void setPositionOffset(@Nullable Vec3d positionOffset) {
-        this.supportPositionOffset = positionOffset;
-    }
-
-    public void updatePositionOffset() {
-        setPositionOffset(supportingEntity == null ? null : entity.getPos().subtract(supportingEntity.getPos()));
-    }
-
-    public void updateRelativePosition(Box box) {
-        if (supportingEntity == null || supportPositionOffset == null) {
-            return;
-        }
-        if (getPhysics().isFlying()) {
-            return;
-        }
-
-        Vec3d newPos = supportingEntity.getPos().add(supportPositionOffset);
-        Vec3d posChange = entity.getPos().subtract(newPos);
-        entity.setPosition(newPos);
-        if (isClient()) {
-            Vec3d newServerPos = LivingEntityDuck.serverPos(entity);
-            if (newServerPos.lengthSquared() != 0) {
-                newServerPos = newServerPos.subtract(posChange);
-                entity.updateTrackedPositionAndAngles(
-                        newServerPos.x, newServerPos.y, newServerPos.z,
-                        entity.getYaw(), entity.getPitch(), 3, true);
-            }
-        } else {
-            entity.updateTrackedPosition(newPos.x, newPos.y, newPos.z);
-        }
-
-        if (!(entity instanceof PlayerEntity)) {
-            entity.lastRenderX = supportingEntity.lastRenderX + supportPositionOffset.x;
-            entity.lastRenderY = supportingEntity.lastRenderY + supportPositionOffset.y;
-            entity.lastRenderZ = supportingEntity.lastRenderZ + supportPositionOffset.z;
-
-            if (entity.getVelocity().length() < 0.1) {
-                LimbAnimationUtil.resetToZero(entity.limbAnimator);
-            }
-        }
-
-        entity.horizontalSpeed = 0;
-        entity.prevHorizontalSpeed = 0;
-        entity.speed = 0;
-        entity.setOnGround(true);
-        entity.verticalCollision = true;
-        entity.groundCollision = true;
-        entity.fallDistance = 0;
-    }
-
     @Override
     public boolean beforeUpdate() {
         if (EffectUtils.getAmplifier(entity, UEffects.PARALYSIS) > 1 && entity.getVelocity().horizontalLengthSquared() > 0) {
@@ -276,44 +205,10 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
             updateVelocity();
         }
 
-        updateSupportingEntity();
+        //transportation.updateSupportingEntity();
         return false;
     }
 
-    public void updateSupportingEntity() {
-        if (supportingEntity != null) {
-            Box ownBox = entity.getBoundingBox()
-                    .stretch(entity.getVelocity())
-                    .expand(0.1, 0.5, 0.1)
-                    .stretch(supportingEntity.getVelocity().multiply(-2));
-
-            MultiBoundingBoxEntity.getBoundingBoxes(supportingEntity).stream()
-            .filter(box -> box.stretch(supportingEntity.getVelocity()).expand(0, 0.5, 0).intersects(ownBox))
-            .findFirst()
-            .ifPresentOrElse(box -> {
-                ticksOutsideVehicle = 0;
-                if (supportPositionOffset == null) {
-                    updatePositionOffset();
-                } else {
-                    updateRelativePosition(box);
-                }
-                entity.setOnGround(true);
-                entity.verticalCollision = true;
-                entity.groundCollision = true;
-            }, () -> {
-                // Rubberband passengers to try and prevent players falling out when the velocity changes suddenly
-                if (ticksOutsideVehicle++ > 30) {
-                    supportingEntity = null;
-                    supportPositionOffset = null;
-                    Unicopia.LOGGER.info("Entity left vehicle");
-                } else {
-                    supportPositionOffset = supportPositionOffset.multiply(0.25, 1, 0.25);
-                }
-            });
-        }
-
-    }
-
     @Override
     public void tick() {
         tickers.forEach(Tickable::tick);
@@ -358,13 +253,7 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
 
         updateDragonBreath();
 
-        if (ticksOutsideVehicle == 0) {
-            updatePositionOffset();
-
-            ticksInVehicle++;
-        } else {
-            ticksInVehicle = 0;
-        }
+        transportation.tick();
     }
 
     public void updateAttributeModifier(UUID id, EntityAttribute attribute, float desiredValue, Float2ObjectFunction<EntityAttributeModifier> modifierSupplier, boolean permanent) {
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Transportation.java b/src/main/java/com/minelittlepony/unicopia/entity/Transportation.java
new file mode 100644
index 00000000..14ad5346
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/entity/Transportation.java
@@ -0,0 +1,141 @@
+package com.minelittlepony.unicopia.entity;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+
+import com.minelittlepony.unicopia.advancement.UCriteria;
+import com.minelittlepony.unicopia.entity.collision.MultiBoundingBoxEntity;
+import com.minelittlepony.unicopia.entity.duck.EntityDuck;
+import com.minelittlepony.unicopia.entity.mob.AirBalloonEntity;
+import com.minelittlepony.unicopia.util.Tickable;
+
+import net.minecraft.block.ShapeContext;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.entity.MovementType;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.world.event.GameEvent;
+
+public class Transportation<T extends LivingEntity> implements Tickable {
+
+    private final Living<T> living;
+
+    @Nullable
+    private MultiBoundingBoxEntity vehicle;
+    @Nullable
+    private Entity vehicleEntity;
+    @Nullable
+    private Box vehicleBox;
+
+    private int ticksInVehicle;
+
+    private Vec3d lastVehiclePosition = Vec3d.ZERO;
+
+    Transportation(Living<T> living) {
+        this.living = living;
+    }
+
+    public <E extends Entity & MultiBoundingBoxEntity> void setVehicle(@Nullable E vehicle) {
+        this.vehicle = vehicle;
+        this.vehicleEntity = vehicle;
+        updatePreviousPosition();
+    }
+
+    @Override
+    public void tick() {
+        if (vehicle != null) {
+            ticksInVehicle++;
+        } else {
+            ticksInVehicle = 0;
+        }
+
+        if (ticksInVehicle > 20 && vehicle instanceof AirBalloonEntity) {
+            UCriteria.RIDE_BALLOON.trigger(living.asEntity());
+        }
+    }
+
+    public void updatePreviousPosition() {
+        vehicleBox = getVehicleBox();
+        lastVehiclePosition = vehicleEntity == null ? Vec3d.ZERO : vehicleEntity.getPos();
+        Entity entity = living.asEntity();
+        if (vehicleBox != null && living.asEntity().getBoundingBox().intersects(vehicleBox.expand(0.001, 0.5001, 0.001))) {
+            entity.setOnGround(true);
+            entity.onLanding();
+            entity.verticalCollision = true;
+            entity.groundCollision = true;
+            entity.velocityDirty = true;
+            entity.velocityModified = true;
+        }
+    }
+
+    public void onMove(MovementType movementType) {
+        if (vehicleBox == null || vehicleEntity == null) {
+            return;
+        }
+
+        Entity entity = living.asEntity();
+
+        Box passengerBox = entity.getBoundingBox().expand(0.001);
+        Vec3d vehicleMovement = vehicleEntity.getPos().subtract(lastVehiclePosition);
+
+        List<VoxelShape> shapes = new ArrayList<>();
+        vehicle.getCollissionShapes(ShapeContext.of(entity), shapes::add);
+        vehicleMovement = vehicleMovement.add(vehicleEntity.getVelocity());
+        vehicleMovement = Entity.adjustMovementForCollisions(entity, vehicleMovement, passengerBox, entity.getWorld(), shapes);
+
+        Vec3d newPos = entity.getPos().add(vehicleMovement);
+
+        if (!vehicleEntity.isOnGround()) {
+            // surface check to prevent the player from floating
+            if (newPos.getY() > vehicleBox.minY + 0.1 || newPos.getY() < vehicleBox.minY + 0.1) {
+                newPos = new Vec3d(newPos.getX(), vehicleBox.minY + 0.01, newPos.getZ());
+            }
+            // containment checks to prevent the player from falling out of the basket when in flight
+            if (newPos.getY() < vehicleEntity.getPos().getY() + 3) {
+                double maxDeviation = 0.1;
+                double z = MathHelper.clamp(newPos.getZ(), vehicleBox.minZ + maxDeviation, vehicleBox.maxZ - maxDeviation);
+                double x = MathHelper.clamp(newPos.getX(), vehicleBox.minX + maxDeviation, vehicleBox.maxX - maxDeviation);
+
+                newPos = new Vec3d(x, newPos.getY(), z);
+            }
+
+            entity.setPosition(newPos);
+            entity.updateTrackedPosition(newPos.x, newPos.y, newPos.z);
+            entity.setVelocity(Vec3d.ZERO);
+        }
+
+        entity.setOnGround(true);
+        entity.onLanding();
+        entity.verticalCollision = true;
+        entity.groundCollision = true;
+
+        if (entity.distanceTraveled > ((EntityDuck)entity).getNextStepSoundDistance()) {
+            entity.distanceTraveled -= 0.5;
+            entity.playSound(vehicle.getWalkedOnSound(entity.getY()), 0.5F, 1);
+            if (!entity.isSneaky()) {
+                entity.getWorld().emitGameEvent(entity, GameEvent.STEP, entity.getBlockPos());
+            }
+        }
+    }
+
+    @Nullable
+    private Box getVehicleBox() {
+        if (vehicle == null) {
+            return null;
+        }
+
+        Box entityBox = living.asEntity().getBoundingBox().stretch(living.asEntity().getVelocity());
+        for (Box box : vehicle.getGravityZoneBoxes()) {
+            if (entityBox.intersects(box.expand(0.001).stretch(vehicleEntity.getVelocity().multiply(1)))) {
+                return box;
+            }
+        }
+
+        setVehicle(null);
+        return null;
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/collision/MultiBoundingBoxEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/collision/MultiBoundingBoxEntity.java
index 82ab40ed..ac7b951e 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/collision/MultiBoundingBoxEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/collision/MultiBoundingBoxEntity.java
@@ -1,13 +1,37 @@
 package com.minelittlepony.unicopia.entity.collision;
 
 import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
 
+import com.minelittlepony.unicopia.entity.collision.EntityCollisions.ComplexCollidable;
+
+import net.minecraft.block.ShapeContext;
 import net.minecraft.entity.Entity;
+import net.minecraft.sound.SoundEvent;
 import net.minecraft.util.math.Box;
+import net.minecraft.util.shape.VoxelShape;
+import net.minecraft.util.shape.VoxelShapes;
 
-public interface MultiBoundingBoxEntity {
+public interface MultiBoundingBoxEntity extends ComplexCollidable {
     List<Box> getBoundingBoxes();
 
+    default List<Box> getGravityZoneBoxes() {
+        return getBoundingBoxes();
+    }
+
+    Map<Box, List<Entity>> getCollidingEntities(Stream<Box> boundingBoxes);
+
+    SoundEvent getWalkedOnSound(double y);
+
+    @Override
+    default void getCollissionShapes(ShapeContext context, Consumer<VoxelShape> output) {
+        for (Box box : getBoundingBoxes()) {
+            output.accept(VoxelShapes.cuboid(box));
+        }
+    }
+
     static List<Box> getBoundingBoxes(Entity entity) {
         return entity instanceof MultiBoundingBoxEntity multi ? multi.getBoundingBoxes() : List.of(entity.getBoundingBox());
     }
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 57cbc2ef..72bf2c6c 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java
@@ -13,7 +13,9 @@ import net.minecraft.item.ItemStack;
 import net.minecraft.item.Items;
 import net.minecraft.nbt.NbtCompound;
 import net.minecraft.particle.ParticleTypes;
+import net.minecraft.predicate.entity.EntityPredicates;
 import net.minecraft.registry.RegistryKey;
+import net.minecraft.sound.SoundEvent;
 import net.minecraft.util.ActionResult;
 import net.minecraft.util.Hand;
 import net.minecraft.util.Identifier;
@@ -22,20 +24,22 @@ import net.minecraft.util.function.ValueLists;
 import net.minecraft.util.math.*;
 import net.minecraft.util.math.random.Random;
 import net.minecraft.util.shape.VoxelShape;
-import net.minecraft.util.shape.VoxelShapes;
 import net.minecraft.world.World;
 import net.minecraft.world.event.GameEvent;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
-import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.IntFunction;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
 import org.jetbrains.annotations.Nullable;
 
 import com.minelittlepony.unicopia.EquineContext;
@@ -47,7 +51,6 @@ import com.minelittlepony.unicopia.entity.MagicImmune;
 import com.minelittlepony.unicopia.entity.collision.EntityCollisions;
 import com.minelittlepony.unicopia.entity.collision.MultiBoundingBoxEntity;
 import com.minelittlepony.unicopia.entity.collision.MultiBox;
-import com.minelittlepony.unicopia.entity.duck.EntityDuck;
 import com.minelittlepony.unicopia.item.BasketItem;
 import com.minelittlepony.unicopia.item.HotAirBalloonItem;
 import com.minelittlepony.unicopia.item.UItems;
@@ -61,13 +64,14 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
     private static final TrackedData<String> BASKET_TYPE = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.STRING);
     private static final TrackedData<Integer> BALLOON_DESIGN = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER);
 
+    private static final Predicate<Entity> RIDER_PREDICATE = EntityPredicates.EXCEPT_SPECTATOR.and(e -> {
+        return !(e instanceof PlayerEntity p && p.getAbilities().flying);
+    });
+
     private boolean prevBoosting;
     private int prevInflation;
-    private Vec3d oldPosition = Vec3d.ZERO;
     private Vec3d manualVelocity = Vec3d.ZERO;
 
-    private int ticksFlying;
-
     public AirBalloonEntity(EntityType<? extends AirBalloonEntity> type, World world) {
         super(type, world);
         intersectionChecked = true;
@@ -80,12 +84,12 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         dataTracker.startTracking(ASCENDING, false);
         dataTracker.startTracking(BOOSTING, 0);
         dataTracker.startTracking(INFLATION, 0);
-        dataTracker.startTracking(BASKET_TYPE, "");
+        dataTracker.startTracking(BASKET_TYPE, BasketType.DEFAULT.id().toString());
         dataTracker.startTracking(BALLOON_DESIGN, 0);
     }
 
     public BasketType getBasketType() {
-        return BasketType.REGISTRY.get(Identifier.tryParse(dataTracker.get(BASKET_TYPE)));
+        return BasketType.of(dataTracker.get(BASKET_TYPE));
     }
 
     public void setBasketType(BasketType type) {
@@ -144,14 +148,6 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         return hasBalloon() && hasBurner() && getInflation() >= getMaxInflation();
     }
 
-    @Override
-    public List<Box> getBoundingBoxes() {
-        if (hasBalloon() && getInflation(1) > 0.999F) {
-            return List.of(getInteriorBoundingBox(), getBalloonBoundingBox());
-        }
-        return List.of(getInteriorBoundingBox());
-    }
-
     @Override
     public void tick() {
         setAir(getMaxAir());
@@ -248,86 +244,12 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         }
 
         prevBoosting = boosting;
-        oldPosition = getPos();
 
         if (getFireTicks() > 0) {
             setFireTicks(1);
         }
 
-        if (!isOnGround() && (isAirworthy() || isSubmergedInWater() || isLeashed())) {
-            ticksFlying++;
-        } else {
-            ticksFlying = 0;
-        }
-
-        updatePassengers(false);
         super.tick();
-        setBoundingBox(MultiBox.of(getBoundingBox(), getBoundingBoxes()));
-    }
-
-    private void updatePassengers(boolean move) {
-        Set<Entity> alreadyTicked = new HashSet<>();
-        for (Box box : getBoundingBoxes()) {
-            for (Entity e : getWorld().getOtherEntities(this, box.stretch(getVelocity().multiply(-1)).expand(0, 0.5, 0))) {
-
-                if (e instanceof PlayerEntity p && p.getAbilities().flying) {
-                    continue;
-                }
-
-                if (!alreadyTicked.add(e)) {
-                    continue;
-                }
-
-                updatePassenger(e, box, e.getY() > getY() + 3);
-            }
-        }
-    }
-
-    private void updatePassenger(Entity e, Box box, boolean inBalloon) {
-
-        if (e instanceof AirBalloonEntity) {
-            return;
-        }
-
-        if (ticksFlying > 0) {
-            if (Living.getOrEmpty(e).filter(living -> !living.setSupportingEntity(this)).isPresent()) {
-                return;
-            }
-
-            Vec3d vel = getVelocity();
-
-            double height = box.getYLength();
-
-            if (height < 3 || e.getBoundingBox().minY > box.minY + height / 2D) {
-                if (vel.y > 0 && e.getBoundingBox().minY < box.maxY + 0.02) {
-                    e.setPos(e.getX(), box.maxY, e.getZ());
-                    e.setOnGround(true);
-                }
-                if (vel.y < 0 && e.getBoundingBox().minY > box.maxY) {
-                    e.setPos(e.getX(), box.maxY, e.getZ());
-                    e.setOnGround(true);
-                }
-            }
-
-            Living.getOrEmpty(e).ifPresent(living -> {
-                living.setPositionOffset(e.getPos().subtract(oldPosition));
-                living.updateRelativePosition(box);
-
-                if (ticksFlying > 20 && living.getTicksInVehicle() > 20) {
-                    UCriteria.RIDE_BALLOON.trigger(e);
-                }
-            });
-        }
-
-        if (getWorld().isClient) {
-            if (e.distanceTraveled > ((EntityDuck)e).getNextStepSoundDistance()) {
-                e.distanceTraveled--;
-                e.playSound(inBalloon ? USounds.ENTITY_HOT_AIR_BALLOON_STEP : USounds.ENTITY_HOT_AIR_BALLOON_BASKET_STEP, 0.5F, 1);
-                if (!e.isSneaky()) {
-                    getWorld().emitGameEvent(e, GameEvent.STEP, getBlockPos());
-                }
-            }
-        }
     }
 
     @Override
@@ -459,12 +381,16 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
     }
 
     @Override
-    protected void fall(double heightDifference, boolean onGround, BlockState state, BlockPos landedPosition) {
+    public Race getSpecies() {
+        return Race.UNSET;
     }
 
     @Override
-    public Race getSpecies() {
-        return Race.UNSET;
+    public SoundEvent getWalkedOnSound(double y) {
+        if (y >= getBalloonBoundingBox().minY) {
+            return USounds.ENTITY_HOT_AIR_BALLOON_STEP;
+        }
+        return USounds.ENTITY_HOT_AIR_BALLOON_BASKET_STEP;
     }
 
     @Override
@@ -497,6 +423,18 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
                     move(MovementType.SELF, getVelocity());
                     setVelocity(getVelocity().multiply(slipperyness));
                 }
+            } else {
+                Map<Box, List<Entity>> collidingEntities = getCollidingEntities(getBoundingBoxes().stream());
+
+                for (Map.Entry<Box, List<Entity>> passengers : collidingEntities.entrySet()) {
+                    for (Entity passenger : passengers.getValue()) {
+                        Living<?> living = Living.living(passenger);
+                        if (living != null) {
+                            living.getTransportation().setVehicle(this);
+                        }
+
+                    }
+                }
             }
             updateLimbs(false);
         }
@@ -507,17 +445,22 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         return false;
     }
 
+    @Override
+    protected Box calculateBoundingBox() {
+        return MultiBox.of(super.calculateBoundingBox(), getBoundingBoxes());
+    }
+
     @Override
     public Box getVisibilityBoundingBox() {
         if (hasBalloon()) {
-            return MultiBox.unbox(getBoundingBox()).union(getBalloonBoundingBox());
+            return getBalloonBoundingBox().withMinY(getY());
         }
-        return MultiBox.unbox(getBoundingBox());
+        return getInteriorBoundingBox();
     }
 
     protected Box getInteriorBoundingBox() {
         Box box = MultiBox.unbox(getBoundingBox());
-        return box.withMinY(box.minY - 0.2).contract(0.2, 0, 0.2);
+        return box.withMinY(box.minY - 0.05).contract(0.15, 0, 0.15);
     }
 
     protected Box getBalloonBoundingBox() {
@@ -528,34 +471,96 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
     }
 
     @Override
-    public void getCollissionShapes(ShapeContext context, Consumer<VoxelShape> output) {
+    public List<Box> getGravityZoneBoxes() {
+        Box balloon = getBalloonBoundingBox().expand(0.001);
+        Box interior = getInteriorBoundingBox().expand(0.001);
+        return List.of(
+                // interior - basket to top of balloon
+                interior.withMaxY(balloon.minY).withMinY(interior.maxY),
+                // balloon
+                balloon.withMaxY(balloon.maxY + 0.5).withMinY(balloon.maxY)
+        );
 
-        Box box = MultiBox.unbox(getBoundingBox()).expand(0.3, 0, 0.3);
+    }
 
-        double wallheight = box.maxY + 0.7;
-        double wallThickness = 0.7;
+    @Override
+    public List<Box> getBoundingBoxes() {
+        List<Box> boxes = new ArrayList<>();
+        Box box = getInteriorBoundingBox();
+        boxes.add(box);
+
+        double wallheight = box.maxY + 0.72;
+        double wallThickness = 0.2;
 
         if (!getBasketType().isOf(BoatEntity.Type.BAMBOO)) {
             // front left (next to door)
-            output.accept(VoxelShapes.cuboid(new Box(box.minX, box.minY, box.minZ, box.minX + wallThickness + 0.2, wallheight, box.minZ + wallThickness)));
+            boxes.add(new Box(box.minX, box.minY, box.minZ, box.minX + wallThickness + 0.4, wallheight, box.minZ + wallThickness));
             // front right (next to door)
-            output.accept(VoxelShapes.cuboid(new Box(box.maxX - wallThickness - 0.2, box.minY, box.minZ, box.maxX, wallheight, box.minZ + wallThickness)));
+            boxes.add(new Box(box.maxX - wallThickness - 0.4, box.minY, box.minZ, box.maxX, wallheight, box.minZ + wallThickness));
 
             // back
-            output.accept(VoxelShapes.cuboid(new Box(box.minX, box.minY, box.maxZ - wallThickness, box.maxX, wallheight, box.maxZ)));
+            boxes.add(new Box(box.minX, box.minY, box.maxZ - wallThickness, box.maxX, wallheight, box.maxZ));
 
             // left
-            output.accept(VoxelShapes.cuboid(new Box(box.maxX - wallThickness, box.minY, box.minZ, box.maxX, wallheight, box.maxZ)));
+            boxes.add(new Box(box.maxX - wallThickness, box.minY, box.minZ, box.maxX, wallheight, box.maxZ));
             // right
-            output.accept(VoxelShapes.cuboid(new Box(box.minX, box.minY, box.minZ, box.minX + wallThickness, wallheight, box.maxZ)));
+            boxes.add(new Box(box.minX, box.minY, box.minZ, box.minX + wallThickness, wallheight, box.maxZ));
         }
 
-        // top of balloon
-        if (hasBalloon() && getInflation() > 0) {
-            output.accept(VoxelShapes.cuboid(getBalloonBoundingBox()));
+        if (hasBalloon() && getInflation(1) > 0.999F) {
+            boxes.add(getBalloonBoundingBox());
+        }
+        return boxes;
+    }
+
+    @Override
+    public void move(MovementType movementType, Vec3d movement) {
+        Vec3d oldPos = this.getPos();
+        List<Box> boundingBoxes = getGravityZoneBoxes();
+        super.move(movementType, movement);
+        if (movementType == MovementType.SELF) {
+            Vec3d actualMovement = getPos().subtract(oldPos);
+            Map<Box, List<Entity>> collidingEntities = getCollidingEntities(
+                    boundingBoxes.stream().map(box -> box.stretch(actualMovement))
+            );
+
+            for (Map.Entry<Box, List<Entity>> passengers : collidingEntities.entrySet()) {
+                for (Entity passenger : passengers.getValue()) {
+                    movePassenger(passenger, actualMovement);
+                }
+            }
         }
     }
 
+    private void movePassenger(Entity passenger, Vec3d movement) {
+        Living<?> living = Living.living(passenger);
+        if (living != null) {
+            if (living.getPhysics().isGravityNegative()) {
+                movement = movement.multiply(1, -1, 1);
+            }
+            living.getTransportation().setVehicle(this);
+        }
+
+        List<VoxelShape> shapes = new ArrayList<>();
+        getCollissionShapes(ShapeContext.of(passenger), shapes::add);
+        movement = Entity.adjustMovementForCollisions(passenger, movement, passenger.getBoundingBox(), getWorld(), shapes);
+
+        passenger.setPosition(passenger.getPos().add(movement));
+        passenger.updateTrackedPosition(passenger.getX(), passenger.getY(), passenger.getZ());
+    }
+
+    @Override
+    public Map<Box, List<Entity>> getCollidingEntities(Stream<Box> boundingBoxes) {
+        return boundingBoxes.collect(Collectors.toMap(Function.identity(), box -> {
+            return getWorld().getOtherEntities(this, box.expand(0.001).stretch(getVelocity().multiply(1)), RIDER_PREDICATE).stream().distinct().toList();
+        }));
+    }
+
+    @Override
+    protected void fall(double heightDifference, boolean onGround, BlockState state, BlockPos landedPosition) {
+    }
+
+
     @Override
     public void readCustomDataFromNbt(NbtCompound compound) {
         super.readCustomDataFromNbt(compound);
@@ -577,6 +582,10 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         compound.putInt("inflationAmount", getInflation());
     }
 
+    static boolean isBetween(double value, double min, double max) {
+        return value >= min && value <= max;
+    }
+
     @SuppressWarnings("deprecation")
     public enum BalloonDesign implements StringIdentifiable {
         NONE,
@@ -607,6 +616,7 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
 
     public record BasketType(Identifier id, @Nullable BoatEntity.Type boatType) {
         private static final Map<Identifier, BasketType> REGISTRY = new HashMap<>();
+        public static final BasketType DEFAULT = of(BoatEntity.Type.OAK);
         static {
             Arrays.stream(BoatEntity.Type.values()).forEach(BasketType::of);
         }
@@ -616,7 +626,7 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         }
 
         public static BasketType of(String name) {
-            Identifier id = Identifier.tryParse(name);
+            Identifier id = name == null || name.isEmpty() ? null : Identifier.tryParse(name);
             if (id == null) {
                 return of(BoatEntity.Type.OAK);
             }
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java
index 6a9c29d3..13644fbb 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java
@@ -21,11 +21,13 @@ import com.minelittlepony.unicopia.entity.duck.EntityDuck;
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.EntityType;
 import net.minecraft.entity.ItemEntity;
+import net.minecraft.entity.MovementType;
 import net.minecraft.entity.Entity.PositionUpdater;
 import net.minecraft.entity.Entity.RemovalReason;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.item.ItemStack;
 import net.minecraft.registry.tag.TagKey;
+import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 
 @Mixin(Entity.class)
@@ -118,4 +120,20 @@ abstract class MixinEntity implements EntityDuck {
             info.setReturnValue(null);
         }
     }
+
+    @Inject(method = "move", at = @At("HEAD"))
+    private void beforeMove(MovementType movementType, Vec3d movement, CallbackInfo info) {
+        Living<?> living = Living.living((Entity)(Object)this);
+        if (living != null) {
+            living.getTransportation().updatePreviousPosition();
+        }
+    }
+
+    @Inject(method = "move", at = @At("RETURN"))
+    private void afterMove(MovementType movementType, Vec3d movement, CallbackInfo info) {
+        Living<?> living = Living.living((Entity)(Object)this);
+        if (living != null) {
+            living.getTransportation().onMove(movementType);
+        }
+    }
 }

From 3ff7466a543efffd232019421c679541d475231f Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 13:47:54 +0000
Subject: [PATCH 04/11] Balloons now require fuel to run

---
 .../unicopia/entity/mob/AirBalloonEntity.java | 75 +++++++++++++++----
 1 file changed, 59 insertions(+), 16 deletions(-)

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 72bf2c6c..690c1a6b 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/AirBalloonEntity.java
@@ -3,6 +3,7 @@ package com.minelittlepony.unicopia.entity.mob;
 import net.fabricmc.fabric.api.tag.convention.v1.ConventionalItemTags;
 import net.minecraft.block.BlockState;
 import net.minecraft.block.ShapeContext;
+import net.minecraft.block.entity.FurnaceBlockEntity;
 import net.minecraft.entity.*;
 import net.minecraft.entity.data.*;
 import net.minecraft.entity.mob.MobEntity;
@@ -72,6 +73,9 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
     private int prevInflation;
     private Vec3d manualVelocity = Vec3d.ZERO;
 
+    private int maxFuel = 100;
+    private int fuel;
+
     public AirBalloonEntity(EntityType<? extends AirBalloonEntity> type, World world) {
         super(type, world);
         intersectionChecked = true;
@@ -174,6 +178,14 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
                 }
                 setInflation(inflation);
             }
+
+            if (fuel > -6 && age % 60 == 0) {
+                fuel -= boosting ? 10 : 1;
+                if (fuel <= -6) {
+                    setBoostTicks(0);
+                    setAscending(false);
+                }
+            }
         } else {
             if (inflation < getMaxInflation() && inflation > 0) {
                 setInflation(--inflation);
@@ -196,15 +208,16 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
             if (hasBurner() && isAscending()) {
                 Vec3d burnerPos = getPos().add(0, 3, 0);
                 for (int i = 0; i < (boosting ? 6 : 1); i++) {
-                    getWorld().addParticle(
-                                getStackInHand(Hand.MAIN_HAND).isOf(Items.SOUL_LANTERN)
+                    getWorld().addParticle(fuel <= 0
+                                ? ParticleTypes.SMOKE
+                                : getStackInHand(Hand.MAIN_HAND).isOf(Items.SOUL_LANTERN)
                                     ? ParticleTypes.SOUL_FIRE_FLAME
                                     : ParticleTypes.FLAME,
                             rng.nextTriangular(burnerPos.x, 0.25),
                             rng.nextTriangular(burnerPos.y, 1),
                             rng.nextTriangular(burnerPos.z, 0.25),
                             0,
-                            Math.max(0, getVelocity().y + (boosting ? 0.1 : 0)),
+                            (boosting ? 0.1 : 0),
                             0
                     );
                 }
@@ -236,7 +249,7 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
             if (leashPost.distanceTo(pos) >= 5) {
                 Vec3d newVel = leashPost.subtract(pos).multiply(0.01);
                 if (isAirworthy()) {
-                    setVelocity(newVel.lengthSquared() < 0.03 ? Vec3d.ZERO : newVel);
+                    setVelocity(newVel.lengthSquared() < 0.0001 ? Vec3d.ZERO : newVel);
                 } else {
                     setVelocity(getVelocity().multiply(0.9).add(newVel));
                 }
@@ -277,11 +290,20 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
                     if (!getWorld().isClient) {
                         manualVelocity = manualVelocity.add(0.3 * xPush, 0, 0.3 * zPush);
                     }
-                } else if (stack.isEmpty() && isAscending()) {
-                    setBoostTicks(50);
+                    getWorld().playSound(null, getX() + hitPos.getX(), getY() + hitPos.getY(), getZ() + hitPos.getZ(), USounds.Vanilla.ENTITY_LEASH_KNOT_PLACE, getSoundCategory(), 1, 1);
                     if (!player.isSneaky()) {
                         getWorld().emitGameEvent(player, GameEvent.ENTITY_INTERACT, getBlockPos());
                     }
+                    return ActionResult.SUCCESS;
+                }
+
+                if (stack.isEmpty() && isAscending()) {
+                    setBoostTicks(50);
+                    playSound(USounds.ENTITY_HOT_AIR_BALLOON_BOOST, 1, 1);
+                    if (!player.isSneaky()) {
+                        getWorld().emitGameEvent(player, GameEvent.ENTITY_INTERACT, getBlockPos());
+                    }
+                    return ActionResult.SUCCESS;
                 }
             }
         }
@@ -309,9 +331,7 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         }
 
         if (stack.isIn(ConventionalItemTags.SHEARS) && hasBalloon()) {
-            if (!player.getAbilities().creativeMode) {
-                stack.damage(1, player, p -> p.sendToolBreakStatus(hand));
-            }
+            stack.damage(1, player, p -> p.sendToolBreakStatus(hand));
             setDesign(BalloonDesign.NONE);
             dropItem(UItems.GIANT_BALLOON);
             playSound(USounds.ENTITY_HOT_AIR_BALLOON_EQUIP_CANOPY, 1, 1);
@@ -336,6 +356,25 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
             return ActionResult.SUCCESS;
         }
 
+        if (hasBurner()) {
+            int fuel = FurnaceBlockEntity.createFuelTimeMap().getOrDefault(stack.getItem(), 0);
+            if (fuel > 0) {
+                if (this.fuel < maxFuel) {
+                    if (this.fuel < 0) {
+                        this.fuel = fuel;
+                    } else {
+                        this.fuel += fuel;
+                    }
+                    if (!player.getAbilities().creativeMode) {
+                        stack.decrement(1);
+                    }
+                    playSound(USounds.Vanilla.ENTITY_VILLAGER_YES, 1, 1);
+                    return ActionResult.SUCCESS;
+                }
+                return ActionResult.FAIL;
+            }
+        }
+
         return ActionResult.PASS;
     }
 
@@ -474,13 +513,15 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
     public List<Box> getGravityZoneBoxes() {
         Box balloon = getBalloonBoundingBox().expand(0.001);
         Box interior = getInteriorBoundingBox().expand(0.001);
-        return List.of(
-                // interior - basket to top of balloon
-                interior.withMaxY(balloon.minY).withMinY(interior.maxY),
-                // balloon
-                balloon.withMaxY(balloon.maxY + 0.5).withMinY(balloon.maxY)
-        );
-
+        if (hasBalloon() && getInflation(1) > 0.999F) {
+            return List.of(
+                    // interior - basket to top of balloon
+                    interior.withMaxY(balloon.minY).withMinY(interior.maxY),
+                    // balloon
+                    balloon.withMaxY(balloon.maxY + 0.5).withMinY(balloon.maxY)
+            );
+        }
+        return List.of(interior.withMaxY(balloon.minY).withMinY(interior.maxY));
     }
 
     @Override
@@ -570,6 +611,7 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         setBoostTicks(compound.getInt("boostTicks"));
         prevInflation = compound.getInt("inflationAmount");
         setInflation(prevInflation);
+        fuel = MathHelper.clamp(compound.getInt("fuel"), 0, maxFuel);
     }
 
     @Override
@@ -580,6 +622,7 @@ public class AirBalloonEntity extends MobEntity implements EntityCollisions.Comp
         compound.putBoolean("burnerActive", isAscending());
         compound.putInt("boostTicks", getBoostTicks());
         compound.putInt("inflationAmount", getInflation());
+        compound.putInt("fuel", fuel);
     }
 
     static boolean isBetween(double value, double min, double max) {

From 101a560a012feeabef96eacbd20d4cdf9abd86e1 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 13:48:12 +0000
Subject: [PATCH 05/11] Improve hot air balloon animations and fix formatting

---
 .../render/entity/AirBalloonEntityModel.java  | 168 +++++++++++-------
 1 file changed, 103 insertions(+), 65 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/client/render/entity/AirBalloonEntityModel.java b/src/main/java/com/minelittlepony/unicopia/client/render/entity/AirBalloonEntityModel.java
index 5934bd8d..03c14bf9 100644
--- a/src/main/java/com/minelittlepony/unicopia/client/render/entity/AirBalloonEntityModel.java
+++ b/src/main/java/com/minelittlepony/unicopia/client/render/entity/AirBalloonEntityModel.java
@@ -22,54 +22,50 @@ public class AirBalloonEntityModel extends EntityModel<AirBalloonEntity> {
 
     private final List<ModelPart> ropes;
 
-	public AirBalloonEntityModel(ModelPart root) {
-	    this.root = root;
-	    isBurner = root.hasChild("burner");
-	    isBalloon = root.hasChild("canopy");
+    public AirBalloonEntityModel(ModelPart root) {
+        this.root = root;
+        isBurner = root.hasChild("burner");
+        isBalloon = root.hasChild("canopy");
 
-	    if (isBurner || isBalloon) {
-	        ModelPart part = root.getChild(isBalloon ? "canopy" : "burner");
-	        ropes = List.of(
-                    part.getChild("rope_a"),
-                    part.getChild("rope_b"),
-                    part.getChild("rope_c"),
-                    part.getChild("rope_d")
-            );
-	    } else {
-	        ropes = List.of();
-	    }
-	}
+        if (isBurner || isBalloon) {
+            ModelPart part = root.getChild(isBalloon ? "canopy" : "burner");
+            ropes = List.of(part.getChild("rope_a"), part.getChild("rope_b"), part.getChild("rope_c"),
+                    part.getChild("rope_d"));
+        } else {
+            ropes = List.of();
+        }
+    }
 
-	public static TexturedModelData getBasketModelData() {
-		ModelData modelData = new ModelData();
-		ModelPartData root = modelData.getRoot();
-		ModelPartData basket = root.addChild("basket", ModelPartBuilder.create().uv(0, 0).cuboid(-16, -1, -16, 32, 2, 30, Dilation.NONE), ModelTransform.pivot(0, 24, 0));
-		basket.addChild("walls", ModelPartBuilder.create().uv(0, 66).cuboid(-17, -12, -16, 2, 11, 30, Dilation.NONE)
-        		.uv(64, 68).cuboid(15, -12, -16, 2, 11, 30, Dilation.NONE)
-        		.uv(80, 38).cuboid(-16, -12, -17, 32, 11, 2, Dilation.NONE)
-        		.uv(0, 32).cuboid(8, -12, 13, 8, 11, 2, Dilation.NONE)
-        		.uv(0, 6).cuboid(-16, -12, 13, 8, 11, 2, Dilation.NONE), ModelTransform.NONE);
-		basket.addChild("rim", ModelPartBuilder.create().uv(40, 34).cuboid(-18, -13, -17, 4, 2, 32, Dilation.NONE)
-        		.uv(0, 32).cuboid(14, -13, -17, 4, 2, 32, Dilation.NONE)
-        		.uv(80, 32).cuboid(-17, -13, -18, 34, 2, 4, Dilation.NONE)
-        		.uv(0, 19).cuboid(7, -13, 12, 10, 2, 4, Dilation.NONE)
-        		.uv(0, 0).cuboid(-17, -13, 12, 10, 2, 4, Dilation.NONE), ModelTransform.NONE);
-		return TexturedModelData.of(modelData, 256, 128);
-	}
+    public static TexturedModelData getBasketModelData() {
+        ModelData modelData = new ModelData();
+        ModelPartData root = modelData.getRoot();
+        ModelPartData basket = root.addChild("basket", ModelPartBuilder.create().uv(0, 0).cuboid(-16, -1, -16, 32, 2, 30, Dilation.NONE), ModelTransform.pivot(0, 24, 0));
+        basket.addChild("walls", ModelPartBuilder.create().uv(0, 66).cuboid(-17, -12, -16, 2, 11, 30, Dilation.NONE)
+                .uv(64, 68).cuboid(15, -12, -16, 2, 11, 30, Dilation.NONE)
+                .uv(80, 38).cuboid(-16, -12, -17, 32, 11, 2, Dilation.NONE)
+                .uv(0, 32).cuboid(8, -12, 13, 8, 11, 2, Dilation.NONE)
+                .uv(0, 6).cuboid(-16, -12, 13, 8, 11, 2, Dilation.NONE), ModelTransform.NONE);
+        basket.addChild("rim", ModelPartBuilder.create().uv(40, 34).cuboid(-18, -13, -17, 4, 2, 32, Dilation.NONE)
+                .uv(0, 32).cuboid(14, -13, -17, 4, 2, 32, Dilation.NONE)
+                .uv(80, 32).cuboid(-17, -13, -18, 34, 2, 4, Dilation.NONE)
+                .uv(0, 19).cuboid(7, -13, 12, 10, 2, 4, Dilation.NONE)
+                .uv(0, 0).cuboid(-17, -13, 12, 10, 2, 4, Dilation.NONE), ModelTransform.NONE);
+        return TexturedModelData.of(modelData, 256, 128);
+    }
 
-	public static TexturedModelData getBurnerModelData() {
+    public static TexturedModelData getBurnerModelData() {
         ModelData modelData = new ModelData();
         ModelPartData root = modelData.getRoot();
 
         ModelPartData burner = root.addChild("burner", ModelPartBuilder.create().uv(8, 0).cuboid(-6, -47, -6, 11, 15, 11, Dilation.NONE), ModelTransform.pivot(0, 24, 0));
-        burner.addChild("rope_d", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of(-5, -46, -6,  0.7854F, 0, -0.7854F));
-        burner.addChild("rope_c", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of(-4, -44,  3, -0.7854F, 0, -0.7854F));
-        burner.addChild("rope_b", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of( 5, -46,  1, -0.7854F, 0,  0.7854F));
-        burner.addChild("rope_a", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of( 5, -45, -6,  0.7854F, 0,  0.7854F));
+        burner.addChild("rope_d", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of(-5, -46, -6, 0.7854F, 0, -0.7854F));
+        burner.addChild("rope_c", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of(-4, -44, 3, -0.7854F, 0, -0.7854F));
+        burner.addChild("rope_b", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of(5, -46, 1, -0.7854F, 0, 0.7854F));
+        burner.addChild("rope_a", ModelPartBuilder.create().cuboid(-2, -68, 0, 2, 68, 2, Dilation.NONE), ModelTransform.of(5, -45, -6, 0.7854F, 0, 0.7854F));
         return TexturedModelData.of(modelData, 64, 128);
     }
 
-	public static TexturedModelData getCanopyModelData() {
+    public static TexturedModelData getCanopyModelData() {
         ModelData modelData = new ModelData();
         ModelPartData root = modelData.getRoot();
         ModelPartData balloon = root.addChild("canopy", ModelPartBuilder.create().cuboid(-54, -178, -59, 112, 120, 112, Dilation.NONE), ModelTransform.pivot(0, 24, 0));
@@ -80,40 +76,82 @@ public class AirBalloonEntityModel extends EntityModel<AirBalloonEntity> {
         return TexturedModelData.of(modelData, 512, 256);
     }
 
-	@Override
-	public void setAngles(AirBalloonEntity entity, float tickDelta, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) {
-	    inflation = entity.getInflation(tickDelta);
-	    root.roll = MathHelper.sin((float)(entity.getX() - entity.prevX));
-        root.pitch = MathHelper.sin((float)(entity.getZ() - entity.prevZ));
+    @Override
+    public void setAngles(AirBalloonEntity entity, float tickDelta, float limbSwingAmount, float ageInTicks,
+            float netHeadYaw, float headPitch) {
+        inflation = entity.getInflation(tickDelta);
 
-	    if (isBurner) {
-	        boolean lifted = inflation > 0.8F;
-	        root.pivotY = 32 * (1 - inflation);
-	        root.pivotX = inflation * MathHelper.sin(limbSwingAmount + entity.age / 5F) / 4F;
-	        ropes.forEach(rope -> rope.visible = lifted);
-	    }
+        if (isBurner || isBalloon) {
+            root.roll = MathHelper.clamp((float) (entity.getX() - entity.lastRenderX), -0.5F, 0.5F);
+            root.pitch = MathHelper.clamp((float) (entity.getZ() - entity.lastRenderZ), -0.5F, 0.5F);
+            if (entity.isLeashed()) {
+                root.roll *= -1;
+                root.pitch *= -1;
+            }
+        } else {
+            root.pitch = 0;
+            root.roll = 0;
+        }
 
-	    if (isBalloon) {
-	        root.pivotY = 0;
-	        root.pivotX = inflation * MathHelper.cos(limbSwingAmount + entity.age / 5F) / 4F;
-	        if (entity.getBasketType().isOf(BoatEntity.Type.BAMBOO)) {
-	            ropes.forEach(rope -> rope.pivotY = 0);
-	        } else {
-	            ropes.forEach(ModelPart::resetTransform);
-	        }
-	    }
-	}
+        for (ModelPart rope : ropes) {
+            rope.resetTransform();
+        }
 
-	@Override
-	public void render(MatrixStack matrices, VertexConsumer vertexConsumer, int light, int overlay, float r, float g, float b, float a) {
-	    if (isBalloon) {
-	        matrices.push();
-	        matrices.translate(0, 1 * (1 - inflation), 0);
+        if (isBurner) {
+            boolean lifted = inflation > 0.8F;
+            root.pivotY = 32 * (1 - inflation);
+            root.pivotX = inflation * MathHelper.sin(limbSwingAmount + entity.age / 5F) / 4F;
+            ropes.forEach(rope -> {
+                rope.visible = lifted;
+                rope.pitch *= 0.125;
+                rope.roll *= 0.125;
+            });
+        }
+        if (isBalloon) {
+            root.pivotY = 0;
+            root.pivotX = inflation * MathHelper.cos(limbSwingAmount + entity.age / 5F) / 4F;
+            if (entity.getBasketType().isOf(BoatEntity.Type.BAMBOO)) {
+                ropes.forEach(rope -> rope.pivotY = 0);
+            } else {
+                ropes.forEach(ModelPart::resetTransform);
+            }
+        }
+
+        for (int i = 0; i < ropes.size(); i++) {
+            ModelPart rope = ropes.get(i);
+            float rollRatio = root.roll / rope.roll;
+            float pitchRatio = root.pitch / rope.pitch;
+
+            rope.pivotY -= 5F * rollRatio;
+            rope.pivotY -= 5F * pitchRatio;
+
+            if (i == 0 || i == 3) {
+                rope.pivotZ -= 5 * pitchRatio;
+            }
+            if (i == 2 || i == 1) {
+                rope.pivotZ += 5 * pitchRatio;
+            }
+
+            if (i == 2 || i == 3) {
+                rope.pivotX -= 5 * rollRatio;
+            }
+            if (i == 0 || i == 1) {
+                rope.pivotX += 5 * rollRatio;
+            }
+        }
+
+    }
+
+    @Override
+    public void render(MatrixStack matrices, VertexConsumer vertexConsumer, int light, int overlay, float r, float g, float b, float a) {
+        if (isBalloon) {
+            matrices.push();
+            matrices.translate(0, 1 * (1 - inflation), 0);
             matrices.scale(1, MathHelper.lerp(inflation, -0.05F, 1), 1);
             root.render(matrices, vertexConsumer, light, overlay, r, g, b, a);
             matrices.pop();
         } else {
             root.render(matrices, vertexConsumer, light, overlay, r, g, b, a);
         }
-	}
+    }
 }
\ No newline at end of file

From 6b3b5c7c86d817d378a30fe199f647a635255251 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 14:51:55 +0000
Subject: [PATCH 06/11] Added baited fishing rod

---
 .../unicopia/UConventionalTags.java           |   2 +-
 .../datagen/providers/UItemTagProvider.java   |   2 +-
 .../datagen/providers/UModelProvider.java     |   4 ++
 .../providers/recipe/URecipeProvider.java     |   7 ++++
 .../unicopia/entity/mob/UEntities.java        |   9 +++-
 .../unicopia/item/BaitedFishingRodItem.java   |  39 ++++++++++++++++++
 .../minelittlepony/unicopia/item/UItems.java  |   1 +
 .../resources/assets/unicopia/lang/en_us.json |   1 +
 .../textures/item/baited_fishing_rod.png      | Bin 0 -> 6420 bytes
 9 files changed, 61 insertions(+), 4 deletions(-)
 create mode 100644 src/main/java/com/minelittlepony/unicopia/item/BaitedFishingRodItem.java
 create mode 100644 src/main/resources/assets/unicopia/textures/item/baited_fishing_rod.png

diff --git a/src/main/java/com/minelittlepony/unicopia/UConventionalTags.java b/src/main/java/com/minelittlepony/unicopia/UConventionalTags.java
index e1a0a0b1..44e8571e 100644
--- a/src/main/java/com/minelittlepony/unicopia/UConventionalTags.java
+++ b/src/main/java/com/minelittlepony/unicopia/UConventionalTags.java
@@ -19,7 +19,7 @@ public interface UConventionalTags {
     TagKey<Item> MUSHROOMS = item("mushrooms");
     TagKey<Item> MUFFINS = item("muffins");
     TagKey<Item> MANGOES = item("mangoes");
-    TagKey<Item> OEATMEALS = item("oatmeals");
+    TagKey<Item> OATMEALS = item("oatmeals");
 
     TagKey<Item> FRUITS = item("fruits");
 
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UItemTagProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UItemTagProvider.java
index 058f636d..65c5d92a 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UItemTagProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UItemTagProvider.java
@@ -169,7 +169,7 @@ public class UItemTagProvider extends FabricTagProvider.ItemTagProvider {
         getOrCreateTagBuilder(UConventionalTags.SEEDS).add(Items.BEETROOT_SEEDS, Items.MELON_SEEDS, Items.PUMPKIN_SEEDS, Items.TORCHFLOWER_SEEDS, Items.WHEAT_SEEDS)
             .add(UItems.OAT_SEEDS)
             .forceAddTag(UTags.APPLE_SEEDS);
-        getOrCreateTagBuilder(UConventionalTags.OEATMEALS).add(UItems.OATMEAL);
+        getOrCreateTagBuilder(UConventionalTags.OATMEALS).add(UItems.OATMEAL);
         getOrCreateTagBuilder(UConventionalTags.GRAIN).add(Items.WHEAT, UItems.OATS);
         getOrCreateTagBuilder(UConventionalTags.NUTS).addOptionalTag(UConventionalTags.CROPS_PEANUTS);
 
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UModelProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UModelProvider.java
index 1c58e4f7..4a652170 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UModelProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UModelProvider.java
@@ -19,6 +19,7 @@ import net.minecraft.item.Items;
 import net.minecraft.registry.Registries;
 import net.minecraft.data.client.ItemModelGenerator;
 import net.minecraft.data.client.ModelIds;
+import net.minecraft.data.client.Models;
 import net.minecraft.data.client.TextureKey;
 import net.minecraft.data.client.TextureMap;
 
@@ -133,5 +134,8 @@ public class UModelProvider extends FabricModelProvider {
                 .addOverride(ModelIds.getItemSubModelId(UItems.GEMSTONE, "_pure"), "affinity", 0)
                 .addOverride(ModelIds.getItemSubModelId(UItems.GEMSTONE, "_corrupted"), "affinity", 1)
                 .upload(UItems.GEMSTONE, itemModelGenerator);
+
+        // fishing rod
+        ItemModels.register(itemModelGenerator, Models.HANDHELD_ROD, UItems.BAITED_FISHING_ROD);
     }
 }
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 57340e26..064c6493 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
@@ -76,6 +76,13 @@ public class URecipeProvider extends FabricRecipeProvider {
             .input(ConventionalItemTags.GLASS_BLOCKS)
             .input(UItems.SUNGLASSES).criterion("has_broken_sunglasses", conditionsFromItem(UItems.BROKEN_SUNGLASSES))
             .offerTo(exporter, convertBetween(UItems.SUNGLASSES, UItems.BROKEN_SUNGLASSES));
+
+        // fishing
+        ShapelessRecipeJsonBuilder.create(RecipeCategory.TOOLS, UItems.BAITED_FISHING_ROD)
+            .input(Items.FISHING_ROD).criterion(hasItem(Items.FISHING_ROD), conditionsFromItem(Items.FISHING_ROD))
+            .input(UItems.WHEAT_WORMS)
+            .group("fishing_rod")
+            .offerTo(exporter);
     }
 
     private void generateVanillaRecipeExtensions(Consumer<RecipeJsonProvider> exporter) {
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 9200384f..8d6ea782 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/mob/UEntities.java
@@ -30,29 +30,34 @@ public interface UEntities {
             .dimensions(EntityDimensions.fixed(0.25F, 0.25F)));
     EntityType<MagicProjectileEntity> THROWN_ITEM = register("thrown_item", FabricEntityTypeBuilder.<MagicProjectileEntity>create(SpawnGroup.MISC, MagicProjectileEntity::new)
             .trackRangeBlocks(100)
+            .disableSummon()
             .trackedUpdateRate(2)
             .dimensions(EntityDimensions.fixed(0.25F, 0.25F)));
     EntityType<PhysicsBodyProjectileEntity> MUFFIN = register("muffin", FabricEntityTypeBuilder.<PhysicsBodyProjectileEntity>create(SpawnGroup.MISC, PhysicsBodyProjectileEntity::new)
             .trackRangeBlocks(100)
+            .disableSummon()
             .trackedUpdateRate(2)
             .dimensions(EntityDimensions.fixed(0.25F, 0.25F)));
     EntityType<MagicBeamEntity> MAGIC_BEAM = register("magic_beam", FabricEntityTypeBuilder.<MagicBeamEntity>create(SpawnGroup.MISC, MagicBeamEntity::new)
             .trackRangeBlocks(100)
+            .disableSummon()
             .trackedUpdateRate(2)
             .dimensions(EntityDimensions.fixed(0.25F, 0.25F)));
     EntityType<FloatingArtefactEntity> FLOATING_ARTEFACT = register("floating_artefact", FabricEntityTypeBuilder.create(SpawnGroup.MISC, FloatingArtefactEntity::new)
             .trackRangeBlocks(200)
+            .disableSummon()
             .dimensions(EntityDimensions.fixed(1, 1)));
     EntityType<CastSpellEntity> CAST_SPELL = register("cast_spell", FabricEntityTypeBuilder.create(SpawnGroup.MISC, CastSpellEntity::new)
             .trackRangeBlocks(200)
+            .disableSummon()
             .dimensions(EntityDimensions.changing(4, 4)));
     EntityType<FairyEntity> TWITTERMITE = register("twittermite", FabricEntityTypeBuilder.create(SpawnGroup.MISC, FairyEntity::new)
             .trackRangeBlocks(200)
             .dimensions(EntityDimensions.fixed(0.1F, 0.1F)));
     EntityType<FriendlyCreeperEntity> FRIENDLY_CREEPER = register("friendly_creeper", FabricEntityTypeBuilder.create(SpawnGroup.MISC, FriendlyCreeperEntity::new)
             .trackRangeChunks(8)
-            .dimensions(EntityDimensions.fixed(0.6f, 1.7f))
-    );
+            .disableSummon()
+            .dimensions(EntityDimensions.fixed(0.6f, 1.7f)));
     EntityType<SpellbookEntity> SPELLBOOK = register("spellbook", FabricEntityTypeBuilder.create(SpawnGroup.MISC, SpellbookEntity::new)
             .trackRangeBlocks(200)
             .dimensions(EntityDimensions.fixed(0.9F, 0.5F)));
diff --git a/src/main/java/com/minelittlepony/unicopia/item/BaitedFishingRodItem.java b/src/main/java/com/minelittlepony/unicopia/item/BaitedFishingRodItem.java
new file mode 100644
index 00000000..6c0a03b3
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/item/BaitedFishingRodItem.java
@@ -0,0 +1,39 @@
+package com.minelittlepony.unicopia.item;
+
+import net.minecraft.enchantment.EnchantmentHelper;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.projectile.FishingBobberEntity;
+import net.minecraft.item.FishingRodItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.util.Hand;
+import net.minecraft.util.TypedActionResult;
+import net.minecraft.world.World;
+
+public class BaitedFishingRodItem extends FishingRodItem {
+
+    public BaitedFishingRodItem(Settings settings) {
+        super(settings);
+    }
+
+    @Override
+    public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
+        TypedActionResult<ItemStack> result = super.use(world, user, hand);
+        if (!world.isClient) {
+            if (user.fishHook != null) {
+                user.fishHook.discard();
+                ItemStack stack = user.getStackInHand(hand);
+                int lure = (EnchantmentHelper.getLure(stack) + 1) * 2;
+                int luck = (EnchantmentHelper.getLuckOfTheSea(stack) + 1) * 2;
+                world.spawnEntity(new FishingBobberEntity(user, world, luck, lure));
+            }
+
+            if (result.getValue().isOf(this)) {
+                ItemStack stack = Items.FISHING_ROD.getDefaultStack();
+                stack.setDamage(result.getValue().getDamage());
+                return TypedActionResult.success(stack, world.isClient());
+            }
+        }
+        return result;
+    }
+}
diff --git a/src/main/java/com/minelittlepony/unicopia/item/UItems.java b/src/main/java/com/minelittlepony/unicopia/item/UItems.java
index ce60d14b..5e365031 100644
--- a/src/main/java/com/minelittlepony/unicopia/item/UItems.java
+++ b/src/main/java/com/minelittlepony/unicopia/item/UItems.java
@@ -89,6 +89,7 @@ public interface UItems {
     Item HORSE_SHOE_FRIES = register("horse_shoe_fries", new Item(new Item.Settings().maxCount(32).food(UFoodComponents.HAY_FRIES)), ItemGroups.FOOD_AND_DRINK);
 
     Item WHEAT_WORMS = register("wheat_worms", new Item(new Item.Settings().maxCount(16).food(UFoodComponents.WORMS)), ItemGroups.NATURAL);
+    Item BAITED_FISHING_ROD = register("baited_fishing_rod", new BaitedFishingRodItem(new Item.Settings().maxDamage(64)), ItemGroups.TOOLS);
     Item MUFFIN = register("muffin", new MuffinItem(new Item.Settings().maxCount(32).food(FoodComponents.BREAD), 0), ItemGroups.FOOD_AND_DRINK);
     Item PINECONE = register("pinecone", new ForageableItem(new Item.Settings().food(UFoodComponents.PINECONE).maxCount(16), () -> Blocks.SPRUCE_LEAVES), ItemGroups.FOOD_AND_DRINK);
     Item ACORN = register("acorn", new ForageableItem(new Item.Settings().food(UFoodComponents.ACORN).maxCount(16), () -> Blocks.OAK_LEAVES), ItemGroups.FOOD_AND_DRINK);
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index 8558c7b3..35f8c79c 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -145,6 +145,7 @@
   "item.unicopia.crispy_hay_fries": "Crispy Hay Fries",
   "item.unicopia.horse_shoe_fries": "Horse Shoe Fries",
   "item.unicopia.wheat_worms": "Wheat Worms",
+  "item.unicopia.baited_fishing_rod": "Baited Fishing Rod",
   "item.unicopia.muffin": "Muffin",
   "item.unicopia.oatmeal_cookie": "Oatmeal Cookie",
   "item.unicopia.pinecone_cookie": "Pinecone Cookie",
diff --git a/src/main/resources/assets/unicopia/textures/item/baited_fishing_rod.png b/src/main/resources/assets/unicopia/textures/item/baited_fishing_rod.png
new file mode 100644
index 0000000000000000000000000000000000000000..90e3a7f69f651bef042d6bc552fcd7dfc54b7602
GIT binary patch
literal 6420
zcmeHKdt4J&7M_qmAb^SnMH(dpEvQX0nY>e?2n30WC4$dgE0bh^C`lj*B%y0pMXS>O
zthM%KeNwHDcCA*c)<P>PD0S<ruCKOkwN<PlLbWelWzU@?JhWZQ{<iy<li!&+bMLv|
zIp25hOy(}i9ycnI6U)IcEK;ADJ|3>UkcEc8Iqqf~iD98d)MS0WUOyBIg*J#}urP){
zj?u{vVlXi#v_A*d8+;UlES3>WUta+KF~7eI{3L%&5cm>C5Ecp76<`GTT)1uqvn=rC
z`WXCUV4?Ic6Fh_*xQ^0iW%CJ<1Q$saFdipl8eFO&RD6j{BU5Q`HAFMkbW{XGu`ui&
z=EFuv0Zs@uzzO*QPO0EC{q-VcLOS`;ejjmX1o<#C(OAEqz(xrIwYQyV`x@&tIzyiU
zi4YAqwS9r5VWA-*?2zD4Hk%z57Rrg>Mn;5(NA&OArzdwnY}~*Bu{<7skSLKacp`zv
zOHvMb0++~SvbaRGRz)O>q%s15Fv7yZBElnLA|qpncwRiw?W1ik78QzWVlo6+P!xk1
z#b`T##X+6H4Epd<K}4CX;1G6b7$+P8KIny%Vg|98!NDvR^v;5Qj1?8!J6<v}q>q7Q
z3re`eoR`*yCZukwjUIozWsub9m>b6F+b^bn?BK@}lZFhH$rVbKT9cNp%NUiZADxps
zVdA98Q>L2CRDMCBWrovLTITk6%PZ#1e{R9TMU_imUiQkXuf6`p@~XGit$%yN#!Z{w
z-@fC6ox65_xM$z~19kNW4>f#x;`5XLICc8W*>e{ze)H|6%Z*pQyW0Bw&09bG_|wn7
z+(x_*&%ohhb|0@O;1$GTF<ETHixK2T#G_cj@sg0<BMoe_q>q4@6Ut3}Y3;V!umtJ&
zmT05nIH&I**@eNah#Jl8_Ys@>UomSZ*1_u>7Qtk|<T0bL5!f&HDwEa({hzI;xvDaW
zxf-30T&b$WlDcU>j!vqYt=1$}Re%0&eO2YKHHY@XdE<fZ;M_tOLyqgBFR#q6;~FN%
z@=CdeX^oOOI_@OHLW7Qr{SISXS!U2-IgtD_&5=(llio}IF8Ksp@-AqmCROcu;i)Hq
zBlbItc^uWg0dg-dUHJ;v@U-`YcRgHc_vIOM(I<`NgSm#3fo>f)oAD51S3%v3aZrTp
z+LJ>;qMPe(0@a#|c^w+j`X$>QGV49lq41!UP=)1GS*}6nZPZ@|y3eq~x@g2`LK=9!
zf&zno!v-#bqJw?&bq2&@rY(kZ^<^ALUzKoRA_Arb_15$Z{|&`DtMn$<pvx;PIcd<j
zSJXq|X&LeNDZ_prDe;X9s)N`E%ngm+4O6!1xD!Jb3;^Z5fA?vPg3IUMM`hsj`_KdP
zz%HRD%~#Gd13;~Yy$Hb*2?)Fia2mK4hO$38?psof3xQ(1v+m0=ppMoW^_yVUkC~sT
zLA)NnjzE|QBpRJI0xh&S8BH#ToPj(VvkTq>SUzrLJ75S(P?8HLn&rTQ2y}5fpy!gm
ztS*#8?s5%(e!CVzLmm*Du87fm%Mb0*1psa!<ar4BQlJy%{!i8}e0z2udON^h+s<OD
zLYWNyMCMu2(z5kwY5Zb`J>OD9VVGydte3Qz`%`#z)Bm(yFo7Lj={l6X^4*F`)v2M}
zwW%AUr<UR$-U?qX8#F|;c<#*F+866KS3JLcOOGvc`9Cu*HhsPBieT_P@sJ%0Tfg2N
zccWR`e4cSHWB#W6NxW?N;mpK7sft}gS<|CZSa)CFyX3&wS>w1lExhSJwY2^qs^4CH
z^sf5$vg!tGbqcRw4;f|eb+l-zrg7Fqe3+ios&3sqvfrR*zunXH{bP+)`=4FaUqO85
zy@+qiZp!<xVoF2yq794Qs@!b&c*M2$_j#Y+K6TT!Q`%p`)4W#djcc-mOk>oTUhydz
zkyLJ8e8xK&m5XH86O;dyuz32Q*T!xkV$^-J=W`lbE81#r*go5La^0KU(b!kwop&W1
zmU~YAS35s>(fW}$h_!0Yu)ke@Pci%KQzx!jh2^Y>B4)qS)~xePk$d)q#A(E!QJiD9
z`f>K&=|3x{M@)5a`ROmuPraj>g%KwwTn$mFSoM|5YMNV{R~<QjPy6}R8|*!s9Q)%I
z@KWLX&|}G+?3$c4R%5bTMWoqoq(mNTG5x=pJi=2<nr2cizL6@h*tCKhAAKU=Tg+O)
zBzYE|Rh&i@S~ANWR8IN0TvPc>liDm8F`Sd^(Exyza*=$GwaDhwc(ei<R|7t377O@v
zh-;=+Fd4q_((De3FB8c`xKQV@lo5jA9DcIHoUa+5p5cdpJFTG5<to;Q#csD-<d%x;
zjsmentyYV1LQD`sh!8rxHW%p;+MEdp!iSMgIZX~rvCCq&@ew9zw3oWH0s-{%1NB*p
zv$ERZZBD-mpoiE)7K<e!Tx_+9JCAU>bY%eIFX&fCICH(llz2Slw3j+el&*}jxe_`<
zm`&~Di%T6vG##@^Ocha92z5eMNtcx9Yq)&`lAyq1Ev83-*j*xBmi!K}y3~exXgZw>
zf#L1AU7`cr>0pS;%F?9UO{J(jeY#eF@@vd?lf|r|T~cn$mk>sYP(l$3q0Fo_3Q46x
z2DeHnX{HELYF2fE(%YOa(q^I%6o88?fI~=3N;Rog2o)+qE|f{7xX`F1)Iy~~Es^Ep
za<vkdbb@%wVS!ai7Im%)L74%{Y$W7zT&573<R+O=rdG>^Ml(eTDXHA7A_<i=UuC4B
z%qC5S-C-qRIxSYRfD#wm3g`|bxMpOwUMnC(ct=Zi5$Vc@0b0RWi>=ht(UEJhQaLUX
zsVPxPBnqjLP^sl|r9z?X2%1PaoUjrRCcUqrd(gCKfD8yrqIC)YbUSd-q&X<kWq0J-
z?L}Ha`;Mr+I}08sGwCAJNf!m6?R%u$z#eHBj%#pV`3Nq{Znorm9~O-s9)2>!!<1)Q
zoRHs3H~F5Z9IC{3=esSk(2o+IPd^kI(&VGyB+Do>T_<4qx=e+nt$>2Jhkv;S+AY7b
z6mXoNaFfz3l$vpgP^Q8aLY0&<2`NH~6Qq(-QYs&-c66sb-{mG9)W`zR5wwEkN$(!{
zN%TYw=~|1skV3Nnf(db{kWk$(nAlHPjNTam#>wIbG)bldI=y6IoUaXDUhpmyx4#Vi
znnAsP<l&!-KQaZM|0qef^nE1PBe}YzK(~S)t*%FMbxVP61wUF{|1-Hb9os3&246vL
z*eorXcW*UpwL*+zMx|qI=(nb3!^_YTR-8G-iDBV=kTJ03D`KFL?b2uI*jJdm9(dTn
zjX{@SHyy1{ADOE<T(@+@%!+Jo-HRuBEY)#>7xZ2^`c#j;drCcH*6zRC<J{4f;T6Z^
zZ(ckn&zZDKx%<r6vz*80L~zDup87&B$+)>;%+XuTm+l_X{?t0VZOM&0Pt421cHa)K
zUUhWqxh4#|J$P%cxu<XTFFIUUetU4s4@dj0inA>-4{mN=eMDKfzqOI`{9oQ#xiT-b
zjMuoMc3VSYpOagy{cE;bmhTU-sv15$-!NeU*%EiFHvPc$C&`!@HDON=w4dD>QT=gN
z>+rj?ukDFUT0d`DZ);jjI5TDGD~Db?TwxyBYfVyRz1coL_TKrXt%r6^<sFQ`EX!x4
zA6V5GUs+XuCFSMTg^Axpx$mSO7(Ha-Xh~GbH1|Kh-8f_Gw9}2#6DGg<R>_z37tt<O
NuN#-XB{gsEzX415U*P}%

literal 0
HcmV?d00001


From 74a11086d1ef041096532a5e0bdd5f22acef4291 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 15:32:00 +0000
Subject: [PATCH 07/11] Added recipe for getting pegasus feathers from gryphon
 feathers

---
 .../providers/UAdvancementsProvider.java      |  1 +
 .../providers/recipe/URecipeProvider.java     | 22 +++++++++++++------
 2 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
index 994082f4..c34a107b 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
@@ -63,6 +63,7 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
                 p.child(Items.CHIPPED_ANVIL).hidden().frame(AdvancementFrame.CHALLENGE).criterion("ding_sun", dingCelestia(Set.of(), Set.of(Race.BAT))).build(consumer, "blasphemy");
                 p.child(Items.CHIPPED_ANVIL).hidden().frame(AdvancementFrame.CHALLENGE).criterion("ding_sun", dingCelestia(Set.of(Race.BAT), Set.of())).build(consumer, "sweet_sweet_revenge");
             });
+            root.child(UItems.BAITED_FISHING_ROD).showToast().announce().criterion("has_baited_fishing_rod", hasItems(UItems.BAITED_FISHING_ROD)).build(consumer, "bait");
             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 -> {
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 064c6493..2161b97a 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
@@ -76,13 +76,6 @@ public class URecipeProvider extends FabricRecipeProvider {
             .input(ConventionalItemTags.GLASS_BLOCKS)
             .input(UItems.SUNGLASSES).criterion("has_broken_sunglasses", conditionsFromItem(UItems.BROKEN_SUNGLASSES))
             .offerTo(exporter, convertBetween(UItems.SUNGLASSES, UItems.BROKEN_SUNGLASSES));
-
-        // fishing
-        ShapelessRecipeJsonBuilder.create(RecipeCategory.TOOLS, UItems.BAITED_FISHING_ROD)
-            .input(Items.FISHING_ROD).criterion(hasItem(Items.FISHING_ROD), conditionsFromItem(Items.FISHING_ROD))
-            .input(UItems.WHEAT_WORMS)
-            .group("fishing_rod")
-            .offerTo(exporter);
     }
 
     private void generateVanillaRecipeExtensions(Consumer<RecipeJsonProvider> exporter) {
@@ -469,6 +462,12 @@ public class URecipeProvider extends FabricRecipeProvider {
 
         // worms
         offerReversibleCompactingRecipes(exporter, RecipeCategory.BUILDING_BLOCKS, UItems.WHEAT_WORMS, RecipeCategory.BUILDING_BLOCKS, UBlocks.WORM_BLOCK);
+        // fishing
+        ShapelessRecipeJsonBuilder.create(RecipeCategory.TOOLS, UItems.BAITED_FISHING_ROD)
+            .input(Items.FISHING_ROD).criterion(hasItem(Items.FISHING_ROD), conditionsFromItem(Items.FISHING_ROD))
+            .input(UItems.WHEAT_WORMS)
+            .group("fishing_rod")
+            .offerTo(exporter);
 
         // utility
         ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, Items.DIRT)
@@ -480,6 +479,15 @@ public class URecipeProvider extends FabricRecipeProvider {
 
         offerShapelessRecipe(exporter, Items.BONE_MEAL, UTags.SHELLS, "bonemeal", 3);
 
+        // pegasus feathers for non pegasi
+        ShapedRecipeJsonBuilder.create(RecipeCategory.MISC, UItems.PEGASUS_FEATHER)
+            .input('*', Items.GHAST_TEAR).criterion("has_ghast_tear", conditionsFromItem(Items.GHAST_TEAR))
+            .input('#', UItems.GRYPHON_FEATHER).criterion("has_feather", conditionsFromItem(UItems.GRYPHON_FEATHER))
+            .pattern("***")
+            .pattern("*#*")
+            .pattern("***")
+            .offerTo(exporter);
+
         offer2x2CompactingRecipe(exporter, RecipeCategory.BUILDING_BLOCKS, Items.COBBLESTONE, UItems.ROCK);
         offerReversibleCompactingRecipesWithReverseRecipeGroup(exporter, RecipeCategory.MISC, UItems.PEBBLES, RecipeCategory.BUILDING_BLOCKS, Blocks.GRAVEL, convertBetween(UItems.PEBBLES, Blocks.GRAVEL), "pebbles");
         offerShapelessRecipe(exporter, UItems.PEBBLES, Blocks.SUSPICIOUS_GRAVEL, "pebbles", 9);

From 82b74ecb18befd405ec8c4037aadc011ed3c7339 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 15:32:21 +0000
Subject: [PATCH 08/11] Added feather touch enchantment

---
 .../unicopia/EquineContext.java               | 11 ++++++++-
 .../unicopia/block/cloud/CloudBlock.java      |  2 +-
 .../block/cloud/CloudPillarBlock.java         | 19 +--------------
 .../unicopia/entity/Living.java               |  7 +++++-
 .../unicopia/item/cloud/CloudBlockItem.java   |  3 +--
 .../item/enchantment/UEnchantments.java       |  9 ++++++++
 .../mixin/MixinEntityShapeContext.java        | 23 +++----------------
 .../resources/assets/unicopia/lang/en_us.json |  4 ++++
 .../tags/items/groups/earth_pony.json         |  1 +
 9 files changed, 36 insertions(+), 43 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/EquineContext.java b/src/main/java/com/minelittlepony/unicopia/EquineContext.java
index 2bd9bf0b..d9dd0a9f 100644
--- a/src/main/java/com/minelittlepony/unicopia/EquineContext.java
+++ b/src/main/java/com/minelittlepony/unicopia/EquineContext.java
@@ -25,11 +25,16 @@ public interface EquineContext {
         return getCompositeRace().canInteractWithClouds();
     }
 
+    default boolean hasFeatherTouch() {
+        return false;
+    }
+
     static EquineContext of(ShapeContext context) {
         if (context == ShapeContext.absent()) {
             return Unicopia.SIDE.getPony().map(EquineContext.class::cast).orElse(ABSENT);
         }
-        return context instanceof EquineContext c ? c : ABSENT;
+        EquineContext result = context instanceof Container c ? c.get() : ABSENT;
+        return result == null ? ABSENT : result;
     }
 
     static EquineContext of(ItemUsageContext context) {
@@ -42,4 +47,8 @@ public interface EquineContext {
         }
         return MoreObjects.firstNonNull(Equine.of(entity).orElse(null), ABSENT);
     }
+
+    interface Container {
+        EquineContext get();
+    }
 }
diff --git a/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudBlock.java b/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudBlock.java
index 490d7e4d..2479021c 100644
--- a/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudBlock.java
+++ b/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudBlock.java
@@ -171,7 +171,7 @@ public class CloudBlock extends Block implements CloudLike {
     }
 
     protected boolean canInteract(BlockState state, BlockView world, BlockPos pos, EquineContext context) {
-        return context.collidesWithClouds();
+        return context.collidesWithClouds() || context.hasFeatherTouch();
     }
 
     @SuppressWarnings("deprecation")
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 baeb2f16..690cbf75 100644
--- a/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudPillarBlock.java
+++ b/src/main/java/com/minelittlepony/unicopia/block/cloud/CloudPillarBlock.java
@@ -63,24 +63,7 @@ public class CloudPillarBlock extends CloudBlock {
 
     @Override
     protected VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context, EquineContext equineContext) {
-        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)];
+        return SHAPES.apply(state.get(AXIS))[(state.get(TOP) ? 0 : 2) + (state.get(BOTTOM) ? 0 : 1)];
     }
 
     @Override
diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Living.java b/src/main/java/com/minelittlepony/unicopia/entity/Living.java
index 86ea4261..fecfe073 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/Living.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/Living.java
@@ -30,6 +30,7 @@ import com.minelittlepony.unicopia.input.Heuristic;
 import com.minelittlepony.unicopia.input.Interactable;
 import com.minelittlepony.unicopia.item.GlassesItem;
 import com.minelittlepony.unicopia.item.UItems;
+import com.minelittlepony.unicopia.item.enchantment.UEnchantments;
 import com.minelittlepony.unicopia.network.datasync.EffectSync;
 import com.minelittlepony.unicopia.network.datasync.Transmittable;
 import com.minelittlepony.unicopia.particle.ParticleUtils;
@@ -198,6 +199,11 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
         return vehicle != null && getCarrierId().filter(vehicle.getUuid()::equals).isPresent();
     }
 
+    @Override
+    public boolean hasFeatherTouch() {
+        return EnchantmentHelper.getEquipmentLevel(UEnchantments.FEATHER_TOUCH, entity) > 0;
+    }
+
     @Override
     public boolean beforeUpdate() {
         if (EffectUtils.getAmplifier(entity, UEffects.PARALYSIS) > 1 && entity.getVelocity().horizontalLengthSquared() > 0) {
@@ -205,7 +211,6 @@ public abstract class Living<T extends LivingEntity> implements Equine<T>, Caste
             updateVelocity();
         }
 
-        //transportation.updateSupportingEntity();
         return false;
     }
 
diff --git a/src/main/java/com/minelittlepony/unicopia/item/cloud/CloudBlockItem.java b/src/main/java/com/minelittlepony/unicopia/item/cloud/CloudBlockItem.java
index f208e723..d4b34d81 100644
--- a/src/main/java/com/minelittlepony/unicopia/item/cloud/CloudBlockItem.java
+++ b/src/main/java/com/minelittlepony/unicopia/item/cloud/CloudBlockItem.java
@@ -19,8 +19,7 @@ import net.minecraft.util.math.Direction;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.World;
 
-public class CloudBlockItem
-extends BlockItem {
+public class CloudBlockItem extends BlockItem {
     public CloudBlockItem(Block block, Item.Settings settings) {
         super(block, settings);
     }
diff --git a/src/main/java/com/minelittlepony/unicopia/item/enchantment/UEnchantments.java b/src/main/java/com/minelittlepony/unicopia/item/enchantment/UEnchantments.java
index 2085f490..a5143773 100644
--- a/src/main/java/com/minelittlepony/unicopia/item/enchantment/UEnchantments.java
+++ b/src/main/java/com/minelittlepony/unicopia/item/enchantment/UEnchantments.java
@@ -40,6 +40,15 @@ public interface UEnchantments {
      */
     Enchantment PADDED = register("padded", new SimpleEnchantment(Options.armor().rarity(Rarity.UNCOMMON).maxLevel(3).traded().table()));
 
+    /**
+     * Allows non-flying races to mine and interact with cloud blocks
+     *
+     * Appears in:
+     *  - Trades
+     *  - Enchanting Table
+     */
+    Enchantment FEATHER_TOUCH = register("feather_touch", new SimpleEnchantment(Options.create(EnchantmentTarget.BREAKABLE, UEnchantmentValidSlots.HANDS).rarity(Rarity.UNCOMMON).traded().table()));
+
     /**
      * Heavy players move more slowly but are less likely to be flung around wildly.
      *
diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntityShapeContext.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntityShapeContext.java
index 9e375403..b0930e75 100644
--- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntityShapeContext.java
+++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntityShapeContext.java
@@ -9,15 +9,13 @@ import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
 import com.minelittlepony.unicopia.EquineContext;
-import com.minelittlepony.unicopia.Race;
-
 import net.minecraft.block.EntityShapeContext;
 import net.minecraft.entity.Entity;
 import net.minecraft.fluid.FluidState;
 import net.minecraft.item.ItemStack;
 
 @Mixin(EntityShapeContext.class)
-abstract class MixinEntityShapeContext implements EquineContext {
+abstract class MixinEntityShapeContext implements EquineContext.Container {
     private EquineContext equineContext;
 
     @Inject(method = "<init>", at = @At("TAIL"))
@@ -26,22 +24,7 @@ abstract class MixinEntityShapeContext implements EquineContext {
     }
 
     @Override
-    public Race getSpecies() {
-        return equineContext.getSpecies();
-    }
-
-    @Override
-    public Race.Composite getCompositeRace() {
-        return equineContext.getCompositeRace();
-    }
-
-    @Override
-    public float getCloudWalkingStrength() {
-        return equineContext.getCloudWalkingStrength();
-    }
-
-    @Override
-    public boolean collidesWithClouds() {
-        return equineContext.collidesWithClouds();
+    public EquineContext get() {
+        return equineContext;
     }
 }
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index 35f8c79c..23edf9ea 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -1339,6 +1339,8 @@
   "enchantment.unicopia.heart_bound.desc": "Causes an item to stay with you after you die",
   "enchantment.unicopia.consumption": "Consumption",
   "enchantment.unicopia.consumption.desc": "Converts drops mined using a tool into raw experience",
+  "enchantment.unicopia.feather_touch": "Feather Touch",
+  "enchantment.unicopia.feather_touch.desc": "Allows breaking and placing cloud blocks when held",
 
   "commands.race.success.self": "Set own race to %1$s",
   "commands.race.success": "%1$s changed race to %2$s",
@@ -1615,6 +1617,8 @@
   "advancements.unicopia.lightning_bug.description": "Attract 10 lightning strikes as a changeling",
   "advancements.unicopia.wonder_bolt.title": "Wonder Bolt",
   "advancements.unicopia.wonder_bolt.description": "Attract 10 lightning strikes",
+  "advancements.unicopia.bait.title": "Is This Bait?",
+  "advancements.unicopia.bait.description": "Put some worms on a hook",
   "advancements.unicopia.jar.title": "Oh wow. What's this?",
   "advancements.unicopia.jar.description": "Find an empty jar",
   "advancements.unicopia.gotcha.title": "Got'cha!",
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 e3ec6fc3..51b9f951 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
@@ -58,6 +58,7 @@
     "unicopia:crispy_hay_fries",
     "unicopia:horse_shoe_fries",
     "unicopia:wheat_worms",
+    "unicopia:baited_fishing_rod",
     "unicopia:worm_block",
     "unicopia:muffin",
     "unicopia:acorn",

From 11f0bbc4e472f3f8119a895ad583b15d1a85b6b7 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 16:23:12 +0000
Subject: [PATCH 09/11] Move advancements back down the tree and add some
 advancements for seaponies and hippogriffs

---
 .../unicopia/ability/ChangeFormAbility.java   |  2 +
 .../unicopia/advancement/UCriteria.java       |  1 +
 .../providers/AdvancementDisplayBuilder.java  |  5 +
 .../providers/UAdvancementsProvider.java      | 95 ++++++++++---------
 .../resources/assets/unicopia/lang/en_us.json |  9 +-
 5 files changed, 67 insertions(+), 45 deletions(-)

diff --git a/src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java
index 64fabfa4..67aa667f 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/ChangeFormAbility.java
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable;
 import com.minelittlepony.unicopia.Race;
 import com.minelittlepony.unicopia.USounds;
 import com.minelittlepony.unicopia.ability.data.Hit;
+import com.minelittlepony.unicopia.advancement.UCriteria;
 import com.minelittlepony.unicopia.entity.player.Pony;
 import com.minelittlepony.unicopia.item.FriendshipBraceletItem;
 
@@ -79,6 +80,7 @@ public class ChangeFormAbility implements Ability<Hit> {
                 Race actualRace = isTransforming ? target.getSpecies() : Race.UNSET;
                 target.setSpecies(supressed.or(player.getCompositeRace().potential()));
                 target.setSuppressedRace(actualRace);
+                UCriteria.SEAPONY_TRANSITION.trigger(target.asEntity());
             }
         });
 
diff --git a/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java b/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java
index 339c0aac..06adc40c 100644
--- a/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java
+++ b/src/main/java/com/minelittlepony/unicopia/advancement/UCriteria.java
@@ -30,6 +30,7 @@ public interface UCriteria {
     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");
+    CustomEventCriterion.Trigger SEAPONY_TRANSITION = CUSTOM_EVENT.createTrigger("seapony_transition");
 
     static void bootstrap() { }
 }
diff --git a/src/main/java/com/minelittlepony/unicopia/datagen/providers/AdvancementDisplayBuilder.java b/src/main/java/com/minelittlepony/unicopia/datagen/providers/AdvancementDisplayBuilder.java
index 40f5d832..cbacbcd3 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/AdvancementDisplayBuilder.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/AdvancementDisplayBuilder.java
@@ -76,6 +76,11 @@ public class AdvancementDisplayBuilder {
         return this;
     }
 
+    public AdvancementDisplayBuilder doNotAnnounce() {
+        this.announce = false;
+        return this;
+    }
+
     public AdvancementDisplayBuilder group(String group) {
         this.group = group;
         return this;
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 c34a107b..e0c2c349 100644
--- a/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
+++ b/src/main/java/com/minelittlepony/unicopia/datagen/providers/UAdvancementsProvider.java
@@ -51,58 +51,21 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
             createTribeRootAdvancement(consumer, root, Race.EARTH).children(consumer, this::generateEarthTribeAdvancementsTree);
             createTribeRootAdvancement(consumer, root, Race.BAT).children(consumer, this::generateBatTribeAdvancementsTree);
             createTribeRootAdvancement(consumer, root, Race.PEGASUS).children(consumer, this::generatePegasusTribeAdvancementsTree);
-            createTribeRootAdvancement(consumer, root, Race.UNICORN).children(consumer, this::generateUnicornTribeAdvancementsTree);
-
-            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.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.BAITED_FISHING_ROD).showToast().announce().criterion("has_baited_fishing_rod", hasItems(UItems.BAITED_FISHING_ROD)).build(consumer, "bait");
-            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).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");
-                });
-                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")
-                 .child(UItems.CIDER).visible().criterion("has_cider", hasItems(UItems.CIDER)).rewards(AdvancementRewards.Builder.experience(12)).build(consumer, "brew_cider");
-            });
+            createTribeRootAdvancement(consumer, root, Race.UNICORN, Race.ALICORN).children(consumer, this::generateUnicornTribeAdvancementsTree);
+            createTribeRootAdvancement(consumer, root, Race.HIPPOGRIFF, Race.SEAPONY).children(consumer, this::generateHippogrifTribeAdvancementsTree);
         });
 
         generateEnchantmentsAdvancementsTree(consumer);
     }
 
-    private AdvancementDisplayBuilder.Parent createTribeRootAdvancement(Consumer<Advancement> consumer, AdvancementDisplayBuilder.Parent root, Race race) {
+    private AdvancementDisplayBuilder.Parent createTribeRootAdvancement(Consumer<Advancement> consumer, AdvancementDisplayBuilder.Parent root, Race race, Race...extra) {
         AdvancementDisplayBuilder builder = root.child(Registries.ITEM.get(race.getId().withSuffixedPath("_badge"))).showToast().announce().group(race.getId().getPath())
                 .criterion("be_" + race.getId().getPath(), new RaceChangeCriterion.Conditions(LootContextPredicate.EMPTY, race));
 
-        if (race == Race.UNICORN) {
-            builder
-                .criterion("be_alicorn", new RaceChangeCriterion.Conditions(LootContextPredicate.EMPTY, Race.ALICORN))
-                .criteriaMerger(CriterionMerger.OR);
+        if (extra.length > 0) {
+            for (Race r : extra) {
+                builder.criterion("be_" + r.getId().getPath(), new RaceChangeCriterion.Conditions(LootContextPredicate.EMPTY, r));
+            }
         }
 
         return builder.build(consumer, race.getId().getPath() + "_route");
@@ -113,6 +76,33 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
             p.child(UItems.PEBBLES).criterion("killed_entity_with_rock", killWithItems(UTags.FROM_ROCKS)).build(consumer, "sticks_and_stones");
             p.child(UItems.WEIRD_ROCK).hidden().criterion("has_rock", hasItems(UItems.WEIRD_ROCK)).build(consumer, "thats_unusual");
         });
+
+        parent.child(UItems.OATS).criterion("has_oats", hasItems(UItems.OATS)).build(consumer, "oats_so_easy");
+        parent.child(Items.HAY_BLOCK).criterion("eat_hay", ConsumeItemCriterion.Conditions.item(Items.HAY_BLOCK)).build(consumer, "what_the_hay");
+        parent.child(UItems.COPPER_HORSE_SHOE).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");
+        });
+        parent.child(UItems.PINECONE).frame(AdvancementFrame.CHALLENGE).criterion("eat_pinecone", ConsumeItemCriterion.Conditions.item(UItems.PINECONE)).build(consumer, "eat_pinecone");
+        parent.child(UItems.OAK_BASKET).doNotAnnounce().criterion("has_basket", hasItems(UTags.BASKETS)).build(consumer, "basket_case")
+            .child(Items.LANTERN).criterion("construct_balloon", CustomEventCriterion.create("construct_balloon")).build(consumer, "aeronaut")
+            .child(UItems.GIANT_BALLOON).announce().frame(AdvancementFrame.CHALLENGE).criterion("ride_balloon", CustomEventCriterion.create("ride_balloon")).build(consumer, "travelling_in_style");
+        parent.child(UItems.MUFFIN).hidden().criterion("has_muffin", hasItems(UItems.MUFFIN)).build(consumer, "baked_bads");
+        parent.child(UItems.HORSE_SHOE_FRIES).criterion("has_horse_shoe_fries", hasItems(UItems.HORSE_SHOE_FRIES)).build(consumer, "lucky");
+        parent.child(UItems.TOAST).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");
+        parent.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");
+            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");
+            });
+            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")
+             .child(UItems.CIDER).visible().criterion("has_cider", hasItems(UItems.CIDER)).rewards(AdvancementRewards.Builder.experience(12)).build(consumer, "brew_cider");
+        });
     }
 
     private void generatePegasusTribeAdvancementsTree(Consumer<Advancement> consumer, AdvancementDisplayBuilder.Parent parent) {
@@ -169,6 +159,17 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
             p.child(Items.WATER_BUCKET).criterion("split_sea", CustomEventCriterion.create("split_sea")).rewards(AdvancementRewards.Builder.experience(105)).build(consumer, "split_the_sea");
         });
 
+        parent.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.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");
+        });
+
         parent.child(UItems.PEGASUS_AMULET).hidden().frame(AdvancementFrame.CHALLENGE).criterion("teleport_above_world", CustomEventCriterion.create("teleport_above_world")).rewards(AdvancementRewards.Builder.experience(100)).build(consumer, "a_falling_wizard");
 
     }
@@ -182,6 +183,12 @@ public class UAdvancementsProvider extends FabricAdvancementProvider {
         });
     }
 
+    private void generateHippogrifTribeAdvancementsTree(Consumer<Advancement> consumer, AdvancementDisplayBuilder.Parent parent) {
+        parent.child(UItems.BAITED_FISHING_ROD).showToast().announce().criterion("has_baited_fishing_rod", hasItems(UItems.BAITED_FISHING_ROD)).build(consumer, "bait");
+        parent.child(UItems.PEARL_NECKLACE).showToast().announce().criterion("seapony_transition",  new CustomEventCriterion.Conditions(LootContextPredicate.EMPTY, "seapony_transition", RacePredicate.of(Set.of(Race.SEAPONY), Set.of()), null, 1)).build(consumer, "shoo_be_doo")
+              .child(UItems.PEARL_NECKLACE).showToast().announce().criterion("seapony_transition", new CustomEventCriterion.Conditions(LootContextPredicate.EMPTY, "seapony_transition", RacePredicate.of(Set.of(), Set.of(Race.SEAPONY)), null, 1)).build(consumer, "shoo_be_done");
+    }
+
     private void generateEnchantmentsAdvancementsTree(Consumer<Advancement> consumer) {
         AdvancementDisplayBuilder.create(Items.NETHERITE_SCRAP).showToast().announce()
             .criterion("enchant_with_consumption", enchant(UEnchantments.CONSUMPTION))
diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json
index 23edf9ea..6b3a168b 100644
--- a/src/main/resources/assets/unicopia/lang/en_us.json
+++ b/src/main/resources/assets/unicopia/lang/en_us.json
@@ -1685,7 +1685,7 @@
   "advancements.unicopia.blasphemy.title": "Blasphemy!",
   "advancements.unicopia.blasphemy.description": "Ding Celestia on the noggin. Oops!",
 
-  "advancements.unicopia.earth_route.title": "Path of the Pony",
+  "advancements.unicopia.earth_route.title": "Hearth of the Earth",
   "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",
@@ -1766,6 +1766,13 @@
   "advancements.unicopia.love_is_power.title": "Love is Power",
   "advancements.unicopia.love_is_power.description": "Banish King Sombra with a crystal heart",
 
+  "advancements.unicopia.hippogriff_route.title": "Splash of Seaquestria",
+  "advancements.unicopia.hippogriff_route.description": "Join the Hippogriff's nest",
+  "advancements.unicopia.shoo_be_doo.title": "Shoo Be Doo!",
+  "advancements.unicopia.shoo_be_doo.description": "Use a pearl necklace to turn into a sea creature",
+  "advancements.unicopia.shoo_be_done.title": "Shoo Be Done!",
+  "advancements.unicopia.shoo_be_done.description": "Use a pearl necklace to turn back to normal",
+
   "unicopia.toast.discoveries.title": "New Discoveries!",
   "unicopia.toast.discoveries.description": "Check your spellbook"
 }

From 5109b67de20943d865395e08692f232f7e791d05 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 16:23:42 +0000
Subject: [PATCH 10/11] Revert seaponies back to normal if we're not able to
 respawn them underwater

---
 .../com/minelittlepony/unicopia/entity/player/Pony.java    | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java
index 0c78666f..10efd22d 100644
--- a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java
+++ b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java
@@ -388,6 +388,13 @@ public class Pony extends Living<PlayerEntity> implements Copyable<Pony>, Update
             boolean mustAvoidAir = getCompositeRace().includes(Race.SEAPONY) && !sw.getFluidState(getOrigin()).isIn(FluidTags.WATER);
             if (mustAvoidSun || mustAvoidAir) {
                 SpawnLocator.selectSpawnPosition(sw, entity, mustAvoidAir, mustAvoidSun);
+                if ((mustAvoidAir && !sw.getFluidState(getOrigin()).isIn(FluidTags.WATER))
+                 || (mustAvoidSun && MeteorlogicalUtil.isPositionExposedToSun(sw, getOrigin()))) {
+                    Race suppressedRace = getSuppressedRace();
+                    if (suppressedRace != Race.UNSET) {
+                        setSpecies(suppressedRace);
+                    }
+                }
             }
         }
         ticksSunImmunity = INITIAL_SUN_IMMUNITY;

From 10dc8ce6d76998e0074932d47fb63b6d1497b440 Mon Sep 17 00:00:00 2001
From: Sollace <sollacea@gmail.com>
Date: Thu, 28 Mar 2024 17:05:04 +0000
Subject: [PATCH 11/11] Check against offline players when trying to see if a
 spell can modify blocks at a location. Closes #283

---
 .../unicopia/ability/magic/Caster.java        | 12 ++++-
 .../magic/spell/effect/HydrophobicSpell.java  | 12 +++--
 .../magic/spell/effect/InfernoSpell.java      |  3 +-
 .../server/world/OfflinePlayerCache.java      | 44 +++++++++++++++++++
 4 files changed, 65 insertions(+), 6 deletions(-)
 create mode 100644 src/main/java/com/minelittlepony/unicopia/server/world/OfflinePlayerCache.java

diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java
index 2d662cb1..dc6f4ec3 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/Caster.java
@@ -14,12 +14,14 @@ import com.minelittlepony.unicopia.entity.damage.UDamageSources;
 import com.minelittlepony.unicopia.particle.ParticleSource;
 import com.minelittlepony.unicopia.server.world.Ether;
 import com.minelittlepony.unicopia.server.world.ModificationType;
+import com.minelittlepony.unicopia.server.world.OfflinePlayerCache;
 import com.minelittlepony.unicopia.util.SoundEmitter;
 import com.minelittlepony.unicopia.util.VecHelper;
 
 import net.minecraft.entity.Entity;
 import net.minecraft.entity.LivingEntity;
 import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.server.world.ServerWorld;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Vec3d;
 import net.minecraft.world.GameRules;
@@ -67,10 +69,18 @@ public interface Caster<E extends Entity> extends
             }
 
             if (getMaster() instanceof PlayerEntity player) {
-                if (!asWorld().canPlayerModifyAt(player, pos)) {
+                if (!player.canModifyBlocks() || !asWorld().canPlayerModifyAt(player, pos)) {
                     return false;
                 }
             } else {
+                if (asWorld() instanceof ServerWorld sw) {
+                    @Nullable
+                    PlayerEntity player = OfflinePlayerCache.getOfflinePlayer(sw, getMasterId().orElse(null));
+                    if (player != null && !player.canModifyBlocks() || !sw.canPlayerModifyAt(player, pos)) {
+                        return false;
+                    }
+                }
+
                 if (!asWorld().getGameRules().getBoolean(GameRules.DO_MOB_GRIEFING)) {
                     return false;
                 }
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/HydrophobicSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/HydrophobicSpell.java
index d88d9dd0..ce21955f 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/HydrophobicSpell.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/HydrophobicSpell.java
@@ -61,7 +61,9 @@ public class HydrophobicSpell extends AbstractSpell {
 
             storedFluidPositions.removeIf(entry -> {
                if (!area.isPointInside(Vec3d.ofCenter(entry.pos()))) {
-                   entry.restore(world);
+                   if (source.canModifyAt(entry.pos())) {
+                       entry.restore(world);
+                   }
                    return true;
                }
 
@@ -72,7 +74,7 @@ public class HydrophobicSpell extends AbstractSpell {
                 pos = new BlockPos(pos);
                 BlockState state = world.getBlockState(pos);
 
-                if (state.getFluidState().isIn(affectedFluid)) {
+                if (source.canModifyAt(pos) && state.getFluidState().isIn(affectedFluid)) {
                     Block block = state.getBlock();
 
                     if (block instanceof FluidBlock) {
@@ -95,7 +97,7 @@ public class HydrophobicSpell extends AbstractSpell {
 
             source.spawnParticles(new Sphere(true, range), 10, pos -> {
                 BlockPos bp = BlockPos.ofFloored(pos);
-                if (source.asWorld().getFluidState(bp.up()).isIn(affectedFluid)) {
+                if (source.canModifyAt(bp) && source.asWorld().getFluidState(bp.up()).isIn(affectedFluid)) {
                     source.addParticle(UParticles.RAIN_DROPS, pos, Vec3d.ZERO);
                 }
             });
@@ -116,7 +118,9 @@ public class HydrophobicSpell extends AbstractSpell {
     protected void onDestroyed(Caster<?> caster) {
         Ether.get(caster.asWorld()).remove(this, caster);
         storedFluidPositions.removeIf(entry -> {
-            entry.restore(caster.asWorld());
+            if (caster.canModifyAt(entry.pos())) {
+                entry.restore(caster.asWorld());
+            }
             return true;
          });
     }
diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java
index 5dc50a69..8c1869ee 100644
--- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java
+++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/InfernoSpell.java
@@ -48,8 +48,9 @@ public class InfernoSpell extends FireSpell {
             for (int i = 0; i < radius * 2; i++) {
                 if (w.random.nextInt(12) == 0) {
                     Vec3d vec = shape.computePoint(w.random).add(origin);
+                    BlockPos pos = BlockPos.ofFloored(vec);
 
-                    if (!applyBlocks(w, BlockPos.ofFloored(vec))) {
+                    if (source.canModifyAt(pos) && !applyBlocks(w, pos)) {
                         applyEntities(source, vec);
                     }
                 }
diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/OfflinePlayerCache.java b/src/main/java/com/minelittlepony/unicopia/server/world/OfflinePlayerCache.java
new file mode 100644
index 00000000..03abf213
--- /dev/null
+++ b/src/main/java/com/minelittlepony/unicopia/server/world/OfflinePlayerCache.java
@@ -0,0 +1,44 @@
+package com.minelittlepony.unicopia.server.world;
+
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.jetbrains.annotations.Nullable;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.mojang.authlib.GameProfile;
+
+import net.fabricmc.fabric.api.entity.FakePlayer;
+import net.minecraft.server.network.ServerPlayerEntity;
+import net.minecraft.server.world.ServerWorld;
+
+public class OfflinePlayerCache {
+    private static final LoadingCache<Key, Optional<ServerPlayerEntity>> CACHE = CacheBuilder.newBuilder()
+            .expireAfterAccess(1, TimeUnit.MINUTES)
+            .build(CacheLoader.from(key -> {
+                ServerPlayerEntity offlinePlayer = FakePlayer.get(key.world(), new GameProfile(key.playerId(), "[Offline Player]"));
+
+                if (key.world().getServer().getPlayerManager().loadPlayerData(offlinePlayer) != null) {
+                    return Optional.of(offlinePlayer);
+                }
+
+                return Optional.empty();
+            }));
+
+    @Nullable
+    public static ServerPlayerEntity getOfflinePlayer(ServerWorld world, UUID playerId) {
+        ServerPlayerEntity player = (ServerPlayerEntity)world.getPlayerByUuid(playerId);
+        if (player == null) {
+            player = world.getServer().getPlayerManager().getPlayer(playerId);
+        }
+        if (player == null) {
+            return CACHE.getUnchecked(new Key(world, playerId)).orElse(null);
+        }
+        return player;
+    }
+
+    record Key (ServerWorld world, UUID playerId) {}
+}