From 2fd8d0c42ff356f4344b216cebbcb745ddb1de75 Mon Sep 17 00:00:00 2001 From: Sollace Date: Fri, 11 Aug 2023 18:51:35 +0100 Subject: [PATCH] Implement the rest of hot air balloons --- .../client/render/WorldRenderDelegate.java | 2 + .../render/entity/AirBalloonEntityModel.java | 4 +- .../unicopia/entity/AirBalloonEntity.java | 237 ++++++++++++------ .../unicopia/entity/Creature.java | 6 +- .../unicopia/entity/Living.java | 89 +++++++ .../entity/MultiBoundingBoxEntity.java | 14 ++ .../unicopia/entity/duck/EntityDuck.java | 3 + .../entity/duck/LivingEntityDuck.java | 12 + .../unicopia/entity/player/Pony.java | 2 +- .../unicopia/item/BasketItem.java | 89 +++++++ .../minelittlepony/unicopia/item/UItems.java | 2 + .../unicopia/mixin/MixinEntity.java | 4 + .../unicopia/mixin/MixinLivingEntity.java | 12 + .../unicopia/textures/entity/air_balloon.png | Bin 3122 -> 12070 bytes 14 files changed, 388 insertions(+), 88 deletions(-) create mode 100644 src/main/java/com/minelittlepony/unicopia/entity/MultiBoundingBoxEntity.java create mode 100644 src/main/java/com/minelittlepony/unicopia/item/BasketItem.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 4d2b5328..b55264fe 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java +++ b/src/main/java/com/minelittlepony/unicopia/client/render/WorldRenderDelegate.java @@ -112,6 +112,8 @@ public class WorldRenderDelegate { return true; } + pony.updateRelativePosition(); + matrices.push(); Entity owner = pony.asEntity(); 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 69c98c4d..c3868fcc 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 @@ -51,8 +51,8 @@ public class AirBalloonEntityModel extends EntityModel { return TexturedModelData.of(modelData, 512, 512); } @Override - public void setAngles(AirBalloonEntity entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) { - root.yaw = entity.bodyYaw; + public void setAngles(AirBalloonEntity entity, float tickDelta, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) { + root.yaw = entity.getBodyYaw(); burner.visible = entity.hasBurner(); balloon.visible = entity.hasBalloon(); diff --git a/src/main/java/com/minelittlepony/unicopia/entity/AirBalloonEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/AirBalloonEntity.java index 8e75aa84..659ab5aa 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/AirBalloonEntity.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/AirBalloonEntity.java @@ -5,26 +5,36 @@ import net.minecraft.entity.*; import net.minecraft.entity.data.*; import net.minecraft.entity.mob.FlyingEntity; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; import net.minecraft.nbt.NbtCompound; -import net.minecraft.network.packet.s2c.play.EntityVelocityUpdateS2CPacket; -import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.sound.SoundEvents; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; 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 java.util.List; import java.util.function.Consumer; import com.minelittlepony.unicopia.entity.collision.EntityCollisions; +import com.minelittlepony.unicopia.entity.duck.EntityDuck; +import com.minelittlepony.unicopia.entity.duck.LivingEntityDuck; +import com.minelittlepony.unicopia.item.UItems; +import com.minelittlepony.unicopia.server.world.WeatherConditions; -public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.ComplexCollidable { +public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.ComplexCollidable, MultiBoundingBoxEntity { private static final byte HAS_BALLOON = 1; private static final byte HAS_BURNER = 2; private static final byte BURNER_ACTIVE = 4; private static final TrackedData FLAGS = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER); + private static final TrackedData BOOSTING = DataTracker.registerData(AirBalloonEntity.class, TrackedDataHandlerRegistry.INTEGER); - private Vec3d prevVehicleVel = Vec3d.ZERO; - private Vec3d velocityBeforeTick = Vec3d.ZERO; + private boolean prevBoosting; public AirBalloonEntity(EntityType type, World world) { super(type, world); @@ -34,6 +44,7 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C protected void initDataTracker() { super.initDataTracker(); dataTracker.startTracking(FLAGS, 0); + dataTracker.startTracking(BOOSTING, 0); } public boolean hasBalloon() { @@ -60,6 +71,14 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C setFlag(BURNER_ACTIVE, burnerActive); } + public int getBoostTicks() { + return dataTracker.get(BOOSTING); + } + + protected void setBoostTicks(int ticks) { + dataTracker.set(BOOSTING, ticks); + } + private boolean getFlag(byte flag) { return (dataTracker.get(FLAGS).intValue() & flag) == flag; } @@ -73,24 +92,70 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C return hasBalloon() && isBurnerActive(); } + @Override + public List getBoundingBoxes() { + if (hasBalloon()) { + Box balloonBox = getBalloonBoundingBox(); + return List.of(getBoundingBox(), balloonBox.withMinY(balloonBox.minY - 0.5)); + } + return List.of(getBoundingBox()); + } + + private Vec3d oldPosition = Vec3d.ZERO; + private Vec3d oldServerPosition = Vec3d.ZERO; + @Override public void tick() { - prevVehicleVel = getVelocity(); setAir(getMaxAir()); + int boostTicks = getBoostTicks(); - if (isAirworthy()) { - setVelocity(getVelocity() - .add(getWind(getWorld(), getBlockPos())) - .normalize() - .multiply(0.2) - .add(0, isBurnerActive() ? 0.00F : isTouchingWater() ? 0.02F : -0.06F, 0)); - } else { - addVelocity(0, -0.03, 0); + if (boostTicks > 0) { + boostTicks--; + setBoostTicks(boostTicks); + } - if (isSubmergedInWater()) { - double yy = getVelocity().y; - setVelocity(getVelocity().multiply(0.9, 0.4, 0.9).add(0, Math.abs(yy) / 2F, 0)); + addVelocity(0, isBurnerActive() ? 0.005 : -0.03, 0); + + if (!isAirworthy() && isSubmergedInWater()) { + double yy = getVelocity().y; + setVelocity(getVelocity().multiply(0.9, 0.4, 0.9).add(0, Math.abs(yy) / 2F, 0)); + } + + boolean boosting = boostTicks > 0; + + Random rng = getWorld().random; + + if (getWorld().isClient()) { + if (hasBurner() && isBurnerActive()) { + Vec3d burnerPos = getPos().add(0, 3, 0); + for (int i = 0; i < (boosting ? 6 : 1); i++) { + getWorld().addParticle(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)), + 0 + ); + } } + } else { + if (hasBurner() && isBurnerActive()) { + addVelocity(WeatherConditions.getAirflow(getBlockPos(), getWorld()).multiply(0.2)); + setVelocity(getVelocity().multiply(0.3, 1, 0.3)); + } + + if (boosting) { + addVelocity(0, 0.02, 0); + } + } + + if (boosting && !prevBoosting) { + playSound(SoundEvents.ENTITY_GHAST_SHOOT, 1, 1); + } + + if (isBurnerActive() && age % 15 + rng.nextInt(5) == 0) { + playSound(SoundEvents.ENTITY_GHAST_SHOOT, 0.2F, 1); } if (isLeashed()) { @@ -103,82 +168,91 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C } } + prevBoosting = boosting; + oldPosition = getPos(); + oldServerPosition = LivingEntityDuck.serverPos(this); + + for (Box box : getBoundingBoxes()) { + for (Entity e : getWorld().getOtherEntities(this, box.expand(0, 0.5, 0))) { + updatePassenger(e, box, e.getY() > getY() + 3); + } + } super.tick(); - - velocityBeforeTick = getVelocity(); - - float weight = 0; - - if (velocityBeforeTick.length() > 0.01 && !isSubmergedInWater()) { - Box box = getInteriorBoundingBox(); - - for (Entity e : getWorld().getOtherEntities(this, box.expand(-0.2, 1, -0.2))) { - updatePassenger(e, box, !isOnGround()); - weight++; - } - - if (hasBalloon()) { - Box balloonBox = getBalloonBoundingBox(); - - for (Entity e : getWorld().getOtherEntities(this, balloonBox.expand(1.0E-7))) { - updatePassenger(e, balloonBox, false); - } - } - } - - if (getVelocity().y > -0.6 && !isTouchingWater()) { - if (isBurnerActive()) { - weight -= 3; - } - addVelocity(0, MathHelper.clamp(-weight / 10F, -1, isLeashed() ? 0.2F : 1), 0); - } } - private void updatePassenger(Entity e, Box box, boolean checkBasket) { - Vec3d pos = e.getPos(); + private void updatePassenger(Entity e, Box box, boolean inBalloon) { - double xx = checkBasket ? MathHelper.clamp(pos.x, box.minX, box.maxX) : pos.x; - double zz = checkBasket ? MathHelper.clamp(pos.z, box.minZ, box.maxZ) : pos.z; - double yy = pos.y; - - Box entityBox = e.getBoundingBox(); - - if ((Math.abs(velocityBeforeTick.y) > 0.0001F && entityBox.minY < box.maxY) - || (entityBox.minY > box.maxY && entityBox.minY < box.maxY + 0.01)) { - yy = box.maxY - Math.signum(velocityBeforeTick.y) * 0.01; + if (getVelocity().y > 0 && e.getBoundingBox().minY < box.maxY) { + e.setPos(e.getX(), box.maxY, e.getZ()); + } + if (getVelocity().y < 0 && e.getBoundingBox().minY > box.maxY) { + e.setPos(e.getX(), box.maxY, e.getZ()); } - if (xx != pos.x || zz != pos.z || yy != pos.y) { - e.setPos(xx + velocityBeforeTick.x, yy, zz + velocityBeforeTick.z); + if (inBalloon && !e.isSneaky() && Math.abs(e.getVelocity().y) > 0.079) { + e.setVelocity(e.getVelocity().multiply(1, e.getVelocity().y < 0 ? -0.9 : 1.2, 1).add(0, 0.8, 0)); + if (Math.abs(e.getVelocity().y) > 2) { + e.setVelocity(e.getVelocity().x, MathHelper.clamp(e.getVelocity().y, -2, 2), e.getVelocity().z); + } } - Vec3d vel = e.getVelocity(); - if (vel.lengthSquared() >= prevVehicleVel.lengthSquared()) { - vel = vel.subtract(prevVehicleVel); - } + Living.getOrEmpty(e).ifPresent(living -> { + living.setSupportingEntity(this); + living.setPositionOffset( + e.getPos().subtract(oldPosition), + LivingEntityDuck.serverPos(living.asEntity()).subtract(oldServerPosition) + ); + living.updateRelativePosition(); + }); - e.setVelocity(vel.multiply(0.5).add(velocityBeforeTick.multiply(0.65))); + if (getWorld().isClient) { + if (e.distanceTraveled > ((EntityDuck)e).getNextStepSoundDistance()) { + e.distanceTraveled--; - Living.updateVelocity(e); - if (e instanceof ServerPlayerEntity ply) { - ply.networkHandler.sendPacket(new EntityVelocityUpdateS2CPacket(ply)); - } - - e.setOnGround(true); - - if (horizontalSpeed != 0) { - e.distanceTraveled = 0; - e.horizontalSpeed = 0; - if (e instanceof LivingEntity l) { - l.limbAnimator.setSpeed(0); - l.limbAnimator.updateLimbs(0, 1); + e.playSound(inBalloon ? SoundEvents.BLOCK_WOOL_STEP : SoundEvents.BLOCK_BAMBOO_STEP, 0.5F, 1); } } } @Override - public void onPlayerCollision(PlayerEntity player) { - // updatePassenger(player, getInteriorBoundingBox(), false); + protected ActionResult interactMob(PlayerEntity player, Hand hand) { + ItemStack stack = player.getStackInHand(hand); + + if (hasBalloon() && hasBurner()) { + if (stack.isOf(Items.FLINT_AND_STEEL)) { + setBurnerActive(!isBurnerActive()); + if (isBurnerActive()) { + playSound(SoundEvents.ENTITY_GHAST_SHOOT, 1, 1); + } + stack.damage(1, player, p -> p.sendEquipmentBreakStatus(hand == Hand.MAIN_HAND ? EquipmentSlot.MAINHAND : EquipmentSlot.OFFHAND)); + playSound(SoundEvents.ITEM_FLINTANDSTEEL_USE, 1, 1); + return ActionResult.SUCCESS; + } + + if (stack.isEmpty() && isBurnerActive()) { + setBoostTicks(50); + } + } + + if (stack.isOf(UItems.LARGE_BALLOON) && !hasBalloon()) { + if (!player.getAbilities().creativeMode) { + stack.decrement(1); + } + playSound(SoundEvents.ITEM_ARMOR_EQUIP_LEATHER, 1, 1); + setHasBalloon(true); + return ActionResult.SUCCESS; + } + + if (stack.isOf(Items.LANTERN) && !hasBurner()) { + if (!player.getAbilities().creativeMode) { + stack.decrement(1); + } + playSound(SoundEvents.ENTITY_IRON_GOLEM_DAMAGE, 0.2F, 1); + setHasBurner(true); + return ActionResult.SUCCESS; + } + + return ActionResult.PASS; } @Override @@ -224,7 +298,6 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C double wallheight = box.maxY + 0.7; double wallThickness = 0.7; - // 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))); // front right (next to door) @@ -240,7 +313,9 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C // top of balloon if (hasBalloon()) { - output.accept(VoxelShapes.cuboid(getBoundingBox().offset(0.125, 6, 0).expand(2.25, 3, 2))); + output.accept(VoxelShapes.cuboid( + getBoundingBox().offset(0.12, 7.5, 0.12).expand(2.4, 3.5, 2.4) + )); } } @@ -250,6 +325,7 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C setHasBalloon(compound.getBoolean("hasBalloon")); setHasBurner(compound.getBoolean("hasBurner")); setBurnerActive(compound.getBoolean("burnerActive")); + setBoostTicks(compound.getInt("boostTicks")); } @Override @@ -258,6 +334,7 @@ public class AirBalloonEntity extends FlyingEntity implements EntityCollisions.C compound.putBoolean("hasBalloon", hasBalloon()); compound.putBoolean("hasBurner", hasBurner()); compound.putBoolean("burnerActive", isBurnerActive()); + compound.putInt("boostTicks", getBoostTicks()); } static Vec3d getWind(World world, BlockPos pos) { diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java index a7cf3124..3831a8fc 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Creature.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Creature.java @@ -152,8 +152,6 @@ public class Creature extends Living implements WeaklyOwned.Mutabl } } - - private void initMinionAi(GoalSelector targets) { clearGoals(targets); targets.add(2, new ActiveEnemyGoal<>(PlayerEntity.class)); @@ -197,9 +195,7 @@ public class Creature extends Living implements WeaklyOwned.Mutabl initDiscordedAi(); } - - - return false; + return super.beforeUpdate(); } private void clearGoals(GoalSelector t) { diff --git a/src/main/java/com/minelittlepony/unicopia/entity/Living.java b/src/main/java/com/minelittlepony/unicopia/entity/Living.java index 06efc0df..a99aaed4 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/Living.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/Living.java @@ -52,6 +52,7 @@ import net.minecraft.server.world.ServerWorld; import net.minecraft.sound.SoundEvents; import net.minecraft.util.Hand; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; @@ -172,6 +173,93 @@ public abstract class Living implements Equine, Caste return vehicle != null && getCarrierId().filter(vehicle.getUuid()::equals).isPresent(); } + @Nullable + private Entity supportingEntity; + @Nullable + private Vec3d supportPositionOffset; + @Nullable + private Vec3d serverPositionOffset; + + public void setSupportingEntity(Entity supportingEntity) { + this.supportingEntity = supportingEntity; + } + + public void setPositionOffset(@Nullable Vec3d positionOffset, @Nullable Vec3d serverPositionOffset) { + this.supportPositionOffset = positionOffset; + this.serverPositionOffset = serverPositionOffset; + } + + public void updatePositionOffset() { + setPositionOffset( + supportingEntity == null ? null : entity.getPos().subtract(supportingEntity.getPos()), + supportingEntity instanceof LivingEntity l ? LivingEntityDuck.serverPos(entity).subtract(LivingEntityDuck.serverPos(l)) : null + ); + } + + public void updateRelativePosition() { + if (supportingEntity == null || supportPositionOffset == null) { + 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.distanceTraveled = 0; + entity.fallDistance = 0; + } + + @Override + public boolean beforeUpdate() { + if (supportingEntity != null) { + Box ownBox = entity.getBoundingBox().expand(0.1); + if (MultiBoundingBoxEntity.getBoundingBoxes(supportingEntity).stream().noneMatch(box -> { + return box.expand(0, 0.5, 0).intersects(ownBox); + })) { + supportingEntity = null; + supportPositionOffset = null; + } + } + if (supportingEntity != null) { + if (supportPositionOffset == null) { + updatePositionOffset(); + } else { + updateRelativePosition(); + } + entity.setOnGround(true); + entity.verticalCollision = true; + entity.groundCollision = true; + } + + return false; + } + @Override public void tick() { tickers.forEach(Tickable::tick); @@ -220,6 +308,7 @@ public abstract class Living implements Equine, Caste } updateDragonBreath(); + updatePositionOffset(); } public void updateAttributeModifier(UUID id, EntityAttribute attribute, float desiredValue, Float2ObjectFunction modifierSupplier, boolean permanent) { diff --git a/src/main/java/com/minelittlepony/unicopia/entity/MultiBoundingBoxEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/MultiBoundingBoxEntity.java new file mode 100644 index 00000000..295dc112 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/entity/MultiBoundingBoxEntity.java @@ -0,0 +1,14 @@ +package com.minelittlepony.unicopia.entity; + +import java.util.List; + +import net.minecraft.entity.Entity; +import net.minecraft.util.math.Box; + +public interface MultiBoundingBoxEntity { + List getBoundingBoxes(); + + static List getBoundingBoxes(Entity entity) { + return entity instanceof MultiBoundingBoxEntity multi ? multi.getBoundingBoxes() : List.of(entity.getBoundingBox()); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/entity/duck/EntityDuck.java b/src/main/java/com/minelittlepony/unicopia/entity/duck/EntityDuck.java index f0633cad..0a61652d 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/duck/EntityDuck.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/duck/EntityDuck.java @@ -6,10 +6,13 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.Entity.RemovalReason; public interface EntityDuck extends LavaAffine, PehkuiEntityExtensions { + void setRemovalReason(RemovalReason reason); void setVehicle(Entity vehicle); + float getNextStepSoundDistance(); + @Override default void setLavaAffine(boolean lavaAffine) { diff --git a/src/main/java/com/minelittlepony/unicopia/entity/duck/LivingEntityDuck.java b/src/main/java/com/minelittlepony/unicopia/entity/duck/LivingEntityDuck.java index 38515145..850e673e 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/duck/LivingEntityDuck.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/duck/LivingEntityDuck.java @@ -1,7 +1,9 @@ package com.minelittlepony.unicopia.entity.duck; +import net.minecraft.entity.LivingEntity; import net.minecraft.item.ItemStack; import net.minecraft.util.Hand; +import net.minecraft.util.math.Vec3d; public interface LivingEntityDuck { void updateItemUsage(Hand hand, ItemStack stack, int time); @@ -16,8 +18,18 @@ public interface LivingEntityDuck { void setLastLeaningPitch(float pitch); + double getServerX(); + + double getServerY(); + + double getServerZ(); + default void copyLeaningAnglesFrom(LivingEntityDuck other) { setLeaningPitch(other.getLeaningPitch()); setLastLeaningPitch(other.getLastLeaningPitch()); } + + static Vec3d serverPos(LivingEntity entity) { + return new Vec3d(((LivingEntityDuck)entity).getServerX(), ((LivingEntityDuck)entity).getServerY(), ((LivingEntityDuck)entity).getServerZ()); + } } 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 d82773fb..dd8f1645 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/player/Pony.java @@ -435,7 +435,7 @@ public class Pony extends Living implements Copyable, Update distanceClimbed = 0; } - return false; + return super.beforeUpdate(); } private boolean isFaceClimbable(World world, BlockPos pos, Direction direction) { diff --git a/src/main/java/com/minelittlepony/unicopia/item/BasketItem.java b/src/main/java/com/minelittlepony/unicopia/item/BasketItem.java new file mode 100644 index 00000000..6161eb58 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/item/BasketItem.java @@ -0,0 +1,89 @@ +package com.minelittlepony.unicopia.item; + +import java.util.function.Predicate; + +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.unicopia.entity.AirBalloonEntity; +import com.minelittlepony.unicopia.entity.UEntities; +import com.minelittlepony.unicopia.util.Dispensable; + +import net.minecraft.block.DispenserBlock; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.BoatItem; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.stat.Stats; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPointer; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.RaycastContext; +import net.minecraft.world.World; + +public class BasketItem extends Item implements Dispensable { + private static final Predicate RIDERS = EntityPredicates.EXCEPT_SPECTATOR.and(Entity::canHit); + private static final double REACH = 5; + + public BasketItem(Item.Settings settings) { + super(settings); + DispenserBlock.registerBehavior(this, createDispenserBehaviour()); + } + + @Override + public TypedActionResult dispenseStack(BlockPointer source, ItemStack stack) { + Direction facing = source.getBlockState().get(DispenserBlock.FACING); + BlockPos pos = source.getPos().offset(facing); + float yaw = facing.getOpposite().asRotation(); + return placeEntity(stack, source.getWorld(), pos.getX(), pos.getY(), pos.getZ(), yaw, null); + } + + @Override + public TypedActionResult use(World world, PlayerEntity user, Hand hand) { + ItemStack stack = user.getStackInHand(hand); + BlockHitResult hit = BoatItem.raycast(world, user, RaycastContext.FluidHandling.ANY); + + if (hit.getType() == HitResult.Type.MISS) { + return TypedActionResult.pass(stack); + } + + Vec3d eyePos = user.getEyePos(); + if (world.getOtherEntities(user, user.getBoundingBox().stretch(user.getRotationVec(1).multiply(REACH)).expand(1), RIDERS).stream() + .anyMatch(entity -> entity.getBoundingBox().expand(entity.getTargetingMargin()).contains(eyePos))) { + return TypedActionResult.pass(stack); + } + + if (hit.getType() == HitResult.Type.BLOCK) { + return placeEntity(stack, world, hit.getPos().x, hit.getPos().y, hit.getPos().z, user.getYaw() + 180, user); + } + + return TypedActionResult.pass(stack); + } + + private TypedActionResult placeEntity(ItemStack stack, World world, double x, double y, double z, float yaw, @Nullable PlayerEntity user) { + AirBalloonEntity entity = UEntities.AIR_BALLOON.create(world); + entity.updatePositionAndAngles(x, y, z, 0, 0); + entity.setHeadYaw(yaw); + entity.setBodyYaw(yaw); + if (!world.isSpaceEmpty(entity, entity.getBoundingBox())) { + return TypedActionResult.fail(stack); + } + if (!world.isClient) { + world.spawnEntity(entity); + if (user != null) { + user.incrementStat(Stats.USED.getOrCreateStat(this)); + } + if (user == null || !user.getAbilities().creativeMode) { + stack.decrement(1); + } + } + + return TypedActionResult.success(stack, world.isClient()); + } +} \ No newline at end of file diff --git a/src/main/java/com/minelittlepony/unicopia/item/UItems.java b/src/main/java/com/minelittlepony/unicopia/item/UItems.java index 70d07536..8e6aa552 100644 --- a/src/main/java/com/minelittlepony/unicopia/item/UItems.java +++ b/src/main/java/com/minelittlepony/unicopia/item/UItems.java @@ -118,6 +118,8 @@ public interface UItems { Item BUTTERFLY = register("butterfly", new Item(new Item.Settings().food(UFoodComponents.INSECTS)), ItemGroups.FOOD_AND_DRINK); Item SPELLBOOK = register("spellbook", new SpellbookItem(new Item.Settings().maxCount(1).rarity(Rarity.UNCOMMON)), ItemGroups.TOOLS); + Item BASKET = register("basket", new BasketItem(new Item.Settings().maxCount(1)), ItemGroups.FUNCTIONAL); + Item LARGE_BALLOON = register("large_balloon", new Item(new Item.Settings().maxCount(1)), ItemGroups.FUNCTIONAL); AmuletItem PEGASUS_AMULET = register("pegasus_amulet", new PegasusAmuletItem(new FabricItemSettings() .maxCount(1) diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java index 4d31bff6..3618c588 100644 --- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java +++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinEntity.java @@ -25,6 +25,10 @@ abstract class MixinEntity implements EntityDuck { @Accessor public abstract void setVehicle(Entity vehicle); + @Override + @Accessor + public abstract float getNextStepSoundDistance(); + @Override public boolean isLavaAffine() { Entity self = (Entity)(Object)this; diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java index a185d11a..917c401c 100644 --- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java +++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinLivingEntity.java @@ -77,6 +77,18 @@ abstract class MixinLivingEntity extends Entity implements LivingEntityDuck, Equ @Accessor("lastLeaningPitch") public abstract void setLastLeaningPitch(float pitch); + @Override + @Accessor + public abstract double getServerX(); + + @Override + @Accessor + public abstract double getServerY(); + + @Override + @Accessor + public abstract double getServerZ(); + @Inject(method = "createLivingAttributes()Lnet/minecraft/entity/attribute/DefaultAttributeContainer$Builder;", at = @At("RETURN")) private static void onCreateAttributes(CallbackInfoReturnable info) { Creature.registerAttributes(info.getReturnValue()); diff --git a/src/main/resources/assets/unicopia/textures/entity/air_balloon.png b/src/main/resources/assets/unicopia/textures/entity/air_balloon.png index 687b373a1fac700e57c72766792ff21c27820246..141d1a73880ce85706d8bed1a29a1f22f9932726 100644 GIT binary patch literal 12070 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