package net.minecraft.world.entity.projectile; import com.mojang.logging.LogUtils; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; import net.minecraft.advancements.CriteriaTriggers; import net.minecraft.core.BlockPos; import net.minecraft.core.particles.ParticleTypes; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.server.level.ServerEntity; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.sounds.SoundEvents; import net.minecraft.stats.Stats; import net.minecraft.tags.FluidTags; import net.minecraft.tags.ItemTags; import net.minecraft.util.Mth; import net.minecraft.util.RandomSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.ExperienceOrb; import net.minecraft.world.entity.MoverType; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FluidState; import net.minecraft.world.level.storage.loot.BuiltInLootTables; import net.minecraft.world.level.storage.loot.LootParams; import net.minecraft.world.level.storage.loot.LootTable; import net.minecraft.world.level.storage.loot.parameters.LootContextParamSets; import net.minecraft.world.level.storage.loot.parameters.LootContextParams; import net.minecraft.world.phys.BlockHitResult; import net.minecraft.world.phys.EntityHitResult; import net.minecraft.world.phys.HitResult; import net.minecraft.world.phys.Vec3; import org.slf4j.Logger; public class FishingHook extends Projectile { private static final Logger LOGGER = LogUtils.getLogger(); private final RandomSource syncronizedRandom = RandomSource.create(); private boolean biting; private int outOfWaterTime; private static final int MAX_OUT_OF_WATER_TIME = 10; private static final EntityDataAccessor DATA_HOOKED_ENTITY = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.INT); private static final EntityDataAccessor DATA_BITING = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.BOOLEAN); private int life; private int nibble; private int timeUntilLured; private int timeUntilHooked; private float fishAngle; private boolean openWater = true; @Nullable private Entity hookedIn; private FishingHook.FishHookState currentState = FishingHook.FishHookState.FLYING; private final int luck; private final int lureSpeed; private FishingHook(EntityType p_150141_, Level p_150142_, int p_150143_, int p_150144_) { super(p_150141_, p_150142_); this.luck = Math.max(0, p_150143_); this.lureSpeed = Math.max(0, p_150144_); } public FishingHook(EntityType p_150138_, Level p_150139_) { this(p_150138_, p_150139_, 0, 0); } public FishingHook(Player p_37106_, Level p_37107_, int p_37108_, int p_37109_) { this(EntityType.FISHING_BOBBER, p_37107_, p_37108_, p_37109_); this.setOwner(p_37106_); float f = p_37106_.getXRot(); float f1 = p_37106_.getYRot(); float f2 = Mth.cos(-f1 * (float) (Math.PI / 180.0) - (float) Math.PI); float f3 = Mth.sin(-f1 * (float) (Math.PI / 180.0) - (float) Math.PI); float f4 = -Mth.cos(-f * (float) (Math.PI / 180.0)); float f5 = Mth.sin(-f * (float) (Math.PI / 180.0)); double d0 = p_37106_.getX() - f3 * 0.3; double d1 = p_37106_.getEyeY(); double d2 = p_37106_.getZ() - f2 * 0.3; this.snapTo(d0, d1, d2, f1, f); Vec3 vec3 = new Vec3(-f3, Mth.clamp(-(f5 / f4), -5.0F, 5.0F), -f2); double d3 = vec3.length(); vec3 = vec3.multiply( 0.6 / d3 + this.random.triangle(0.5, 0.0103365), 0.6 / d3 + this.random.triangle(0.5, 0.0103365), 0.6 / d3 + this.random.triangle(0.5, 0.0103365) ); this.setDeltaMovement(vec3); this.setYRot((float)(Mth.atan2(vec3.x, vec3.z) * 180.0F / (float)Math.PI)); this.setXRot((float)(Mth.atan2(vec3.y, vec3.horizontalDistance()) * 180.0F / (float)Math.PI)); this.yRotO = this.getYRot(); this.xRotO = this.getXRot(); } @Override protected void defineSynchedData(SynchedEntityData.Builder p_330190_) { p_330190_.define(DATA_HOOKED_ENTITY, 0); p_330190_.define(DATA_BITING, false); } @Override protected boolean shouldBounceOnWorldBorder() { return true; } @Override public void onSyncedDataUpdated(EntityDataAccessor p_37153_) { if (DATA_HOOKED_ENTITY.equals(p_37153_)) { int i = this.getEntityData().get(DATA_HOOKED_ENTITY); this.hookedIn = i > 0 ? this.level().getEntity(i - 1) : null; } if (DATA_BITING.equals(p_37153_)) { this.biting = this.getEntityData().get(DATA_BITING); if (this.biting) { this.setDeltaMovement(this.getDeltaMovement().x, -0.4F * Mth.nextFloat(this.syncronizedRandom, 0.6F, 1.0F), this.getDeltaMovement().z); } } super.onSyncedDataUpdated(p_37153_); } @Override public boolean shouldRenderAtSqrDistance(double p_37125_) { double d0 = 64.0; return p_37125_ < 4096.0; } @Override public void tick() { this.syncronizedRandom.setSeed(this.getUUID().getLeastSignificantBits() ^ this.level().getGameTime()); super.tick(); Player player = this.getPlayerOwner(); if (player == null) { this.discard(); } else if (this.level().isClientSide || !this.shouldStopFishing(player)) { if (this.onGround()) { this.life++; if (this.life >= 1200) { this.discard(); return; } } else { this.life = 0; } float f = 0.0F; BlockPos blockpos = this.blockPosition(); FluidState fluidstate = this.level().getFluidState(blockpos); if (fluidstate.is(FluidTags.WATER)) { f = fluidstate.getHeight(this.level(), blockpos); } boolean flag = f > 0.0F; if (this.currentState == FishingHook.FishHookState.FLYING) { if (this.hookedIn != null) { this.setDeltaMovement(Vec3.ZERO); this.currentState = FishingHook.FishHookState.HOOKED_IN_ENTITY; return; } if (flag) { this.setDeltaMovement(this.getDeltaMovement().multiply(0.3, 0.2, 0.3)); this.currentState = FishingHook.FishHookState.BOBBING; return; } this.checkCollision(); } else { if (this.currentState == FishingHook.FishHookState.HOOKED_IN_ENTITY) { if (this.hookedIn != null) { if (!this.hookedIn.isRemoved() && this.hookedIn.level().dimension() == this.level().dimension()) { this.setPos(this.hookedIn.getX(), this.hookedIn.getY(0.8), this.hookedIn.getZ()); } else { this.setHookedEntity(null); this.currentState = FishingHook.FishHookState.FLYING; } } return; } if (this.currentState == FishingHook.FishHookState.BOBBING) { Vec3 vec3 = this.getDeltaMovement(); double d0 = this.getY() + vec3.y - blockpos.getY() - f; if (Math.abs(d0) < 0.01) { d0 += Math.signum(d0) * 0.1; } this.setDeltaMovement(vec3.x * 0.9, vec3.y - d0 * this.random.nextFloat() * 0.2, vec3.z * 0.9); if (this.nibble <= 0 && this.timeUntilHooked <= 0) { this.openWater = true; } else { this.openWater = this.openWater && this.outOfWaterTime < 10 && this.calculateOpenWater(blockpos); } if (flag) { this.outOfWaterTime = Math.max(0, this.outOfWaterTime - 1); if (this.biting) { this.setDeltaMovement(this.getDeltaMovement().add(0.0, -0.1 * this.syncronizedRandom.nextFloat() * this.syncronizedRandom.nextFloat(), 0.0)); } if (!this.level().isClientSide) { this.catchingFish(blockpos); } } else { this.outOfWaterTime = Math.min(10, this.outOfWaterTime + 1); } } } if (!fluidstate.is(FluidTags.WATER)) { this.setDeltaMovement(this.getDeltaMovement().add(0.0, -0.03, 0.0)); } this.move(MoverType.SELF, this.getDeltaMovement()); this.applyEffectsFromBlocks(); this.updateRotation(); if (this.currentState == FishingHook.FishHookState.FLYING && (this.onGround() || this.horizontalCollision)) { this.setDeltaMovement(Vec3.ZERO); } double d1 = 0.92; this.setDeltaMovement(this.getDeltaMovement().scale(0.92)); this.reapplyPosition(); } } private boolean shouldStopFishing(Player p_37137_) { ItemStack itemstack = p_37137_.getMainHandItem(); ItemStack itemstack1 = p_37137_.getOffhandItem(); boolean flag = itemstack.is(Items.FISHING_ROD); boolean flag1 = itemstack1.is(Items.FISHING_ROD); if (!p_37137_.isRemoved() && p_37137_.isAlive() && (flag || flag1) && !(this.distanceToSqr(p_37137_) > 1024.0)) { return false; } else { this.discard(); return true; } } private void checkCollision() { HitResult hitresult = ProjectileUtil.getHitResultOnMoveVector(this, this::canHitEntity); this.hitTargetOrDeflectSelf(hitresult); } @Override protected boolean canHitEntity(Entity p_37135_) { return super.canHitEntity(p_37135_) || p_37135_.isAlive() && p_37135_ instanceof ItemEntity; } @Override protected void onHitEntity(EntityHitResult p_37144_) { super.onHitEntity(p_37144_); if (!this.level().isClientSide) { this.setHookedEntity(p_37144_.getEntity()); } } @Override protected void onHitBlock(BlockHitResult p_37142_) { super.onHitBlock(p_37142_); this.setDeltaMovement(this.getDeltaMovement().normalize().scale(p_37142_.distanceTo(this))); } private void setHookedEntity(@Nullable Entity p_150158_) { this.hookedIn = p_150158_; this.getEntityData().set(DATA_HOOKED_ENTITY, p_150158_ == null ? 0 : p_150158_.getId() + 1); } private void catchingFish(BlockPos p_37146_) { ServerLevel serverlevel = (ServerLevel)this.level(); int i = 1; BlockPos blockpos = p_37146_.above(); if (this.random.nextFloat() < 0.25F && this.level().isRainingAt(blockpos)) { i++; } if (this.random.nextFloat() < 0.5F && !this.level().canSeeSky(blockpos)) { i--; } if (this.nibble > 0) { this.nibble--; if (this.nibble <= 0) { this.timeUntilLured = 0; this.timeUntilHooked = 0; this.getEntityData().set(DATA_BITING, false); } } else if (this.timeUntilHooked > 0) { this.timeUntilHooked -= i; if (this.timeUntilHooked > 0) { this.fishAngle = this.fishAngle + (float)this.random.triangle(0.0, 9.188); float f = this.fishAngle * (float) (Math.PI / 180.0); float f1 = Mth.sin(f); float f2 = Mth.cos(f); double d0 = this.getX() + f1 * this.timeUntilHooked * 0.1F; double d1 = Mth.floor(this.getY()) + 1.0F; double d2 = this.getZ() + f2 * this.timeUntilHooked * 0.1F; BlockState blockstate = serverlevel.getBlockState(BlockPos.containing(d0, d1 - 1.0, d2)); if (blockstate.is(Blocks.WATER)) { if (this.random.nextFloat() < 0.15F) { serverlevel.sendParticles(ParticleTypes.BUBBLE, d0, d1 - 0.1F, d2, 1, f1, 0.1, f2, 0.0); } float f3 = f1 * 0.04F; float f4 = f2 * 0.04F; serverlevel.sendParticles(ParticleTypes.FISHING, d0, d1, d2, 0, f4, 0.01, -f3, 1.0); serverlevel.sendParticles(ParticleTypes.FISHING, d0, d1, d2, 0, -f4, 0.01, f3, 1.0); } } else { this.playSound(SoundEvents.FISHING_BOBBER_SPLASH, 0.25F, 1.0F + (this.random.nextFloat() - this.random.nextFloat()) * 0.4F); double d3 = this.getY() + 0.5; serverlevel.sendParticles( ParticleTypes.BUBBLE, this.getX(), d3, this.getZ(), (int)(1.0F + this.getBbWidth() * 20.0F), this.getBbWidth(), 0.0, this.getBbWidth(), 0.2F ); serverlevel.sendParticles( ParticleTypes.FISHING, this.getX(), d3, this.getZ(), (int)(1.0F + this.getBbWidth() * 20.0F), this.getBbWidth(), 0.0, this.getBbWidth(), 0.2F ); this.nibble = Mth.nextInt(this.random, 20, 40); this.getEntityData().set(DATA_BITING, true); } } else if (this.timeUntilLured > 0) { this.timeUntilLured -= i; float f5 = 0.15F; if (this.timeUntilLured < 20) { f5 += (20 - this.timeUntilLured) * 0.05F; } else if (this.timeUntilLured < 40) { f5 += (40 - this.timeUntilLured) * 0.02F; } else if (this.timeUntilLured < 60) { f5 += (60 - this.timeUntilLured) * 0.01F; } if (this.random.nextFloat() < f5) { float f6 = Mth.nextFloat(this.random, 0.0F, 360.0F) * (float) (Math.PI / 180.0); float f7 = Mth.nextFloat(this.random, 25.0F, 60.0F); double d4 = this.getX() + Mth.sin(f6) * f7 * 0.1; double d5 = Mth.floor(this.getY()) + 1.0F; double d6 = this.getZ() + Mth.cos(f6) * f7 * 0.1; BlockState blockstate1 = serverlevel.getBlockState(BlockPos.containing(d4, d5 - 1.0, d6)); if (blockstate1.is(Blocks.WATER)) { serverlevel.sendParticles(ParticleTypes.SPLASH, d4, d5, d6, 2 + this.random.nextInt(2), 0.1F, 0.0, 0.1F, 0.0); } } if (this.timeUntilLured <= 0) { this.fishAngle = Mth.nextFloat(this.random, 0.0F, 360.0F); this.timeUntilHooked = Mth.nextInt(this.random, 20, 80); } } else { this.timeUntilLured = Mth.nextInt(this.random, 100, 600); this.timeUntilLured = this.timeUntilLured - this.lureSpeed; } } private boolean calculateOpenWater(BlockPos p_37159_) { FishingHook.OpenWaterType fishinghook$openwatertype = FishingHook.OpenWaterType.INVALID; for (int i = -1; i <= 2; i++) { FishingHook.OpenWaterType fishinghook$openwatertype1 = this.getOpenWaterTypeForArea(p_37159_.offset(-2, i, -2), p_37159_.offset(2, i, 2)); switch (fishinghook$openwatertype1) { case ABOVE_WATER: if (fishinghook$openwatertype == FishingHook.OpenWaterType.INVALID) { return false; } break; case INSIDE_WATER: if (fishinghook$openwatertype == FishingHook.OpenWaterType.ABOVE_WATER) { return false; } break; case INVALID: return false; } fishinghook$openwatertype = fishinghook$openwatertype1; } return true; } private FishingHook.OpenWaterType getOpenWaterTypeForArea(BlockPos p_37148_, BlockPos p_37149_) { return BlockPos.betweenClosedStream(p_37148_, p_37149_) .map(this::getOpenWaterTypeForBlock) .reduce((p_37139_, p_37140_) -> p_37139_ == p_37140_ ? p_37139_ : FishingHook.OpenWaterType.INVALID) .orElse(FishingHook.OpenWaterType.INVALID); } private FishingHook.OpenWaterType getOpenWaterTypeForBlock(BlockPos p_37164_) { BlockState blockstate = this.level().getBlockState(p_37164_); if (!blockstate.isAir() && !blockstate.is(Blocks.LILY_PAD)) { FluidState fluidstate = blockstate.getFluidState(); return fluidstate.is(FluidTags.WATER) && fluidstate.isSource() && blockstate.getCollisionShape(this.level(), p_37164_).isEmpty() ? FishingHook.OpenWaterType.INSIDE_WATER : FishingHook.OpenWaterType.INVALID; } else { return FishingHook.OpenWaterType.ABOVE_WATER; } } public boolean isOpenWaterFishing() { return this.openWater; } @Override public void addAdditionalSaveData(CompoundTag p_37161_) { } @Override public void readAdditionalSaveData(CompoundTag p_37151_) { } public int retrieve(ItemStack p_37157_) { Player player = this.getPlayerOwner(); if (!this.level().isClientSide && player != null && !this.shouldStopFishing(player)) { int i = 0; if (this.hookedIn != null) { this.pullEntity(this.hookedIn); CriteriaTriggers.FISHING_ROD_HOOKED.trigger((ServerPlayer)player, p_37157_, this, Collections.emptyList()); this.level().broadcastEntityEvent(this, (byte)31); i = this.hookedIn instanceof ItemEntity ? 3 : 5; } else if (this.nibble > 0) { LootParams lootparams = new LootParams.Builder((ServerLevel)this.level()) .withParameter(LootContextParams.ORIGIN, this.position()) .withParameter(LootContextParams.TOOL, p_37157_) .withParameter(LootContextParams.THIS_ENTITY, this) .withLuck(this.luck + player.getLuck()) .create(LootContextParamSets.FISHING); LootTable loottable = this.level().getServer().reloadableRegistries().getLootTable(BuiltInLootTables.FISHING); List list = loottable.getRandomItems(lootparams); CriteriaTriggers.FISHING_ROD_HOOKED.trigger((ServerPlayer)player, p_37157_, this, list); for (ItemStack itemstack : list) { ItemEntity itementity = new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), itemstack); double d0 = player.getX() - this.getX(); double d1 = player.getY() - this.getY(); double d2 = player.getZ() - this.getZ(); double d3 = 0.1; itementity.setDeltaMovement(d0 * 0.1, d1 * 0.1 + Math.sqrt(Math.sqrt(d0 * d0 + d1 * d1 + d2 * d2)) * 0.08, d2 * 0.1); this.level().addFreshEntity(itementity); player.level() .addFreshEntity( new ExperienceOrb( player.level(), player.getX(), player.getY() + 0.5, player.getZ() + 0.5, this.random.nextInt(6) + 1 ) ); if (itemstack.is(ItemTags.FISHES)) { player.awardStat(Stats.FISH_CAUGHT, 1); } } i = 1; } if (this.onGround()) { i = 2; } this.discard(); return i; } else { return 0; } } @Override public void handleEntityEvent(byte p_37123_) { if (p_37123_ == 31 && this.level().isClientSide && this.hookedIn instanceof Player player && player.isLocalPlayer()) { this.pullEntity(this.hookedIn); } super.handleEntityEvent(p_37123_); } protected void pullEntity(Entity p_150156_) { Entity entity = this.getOwner(); if (entity != null) { Vec3 vec3 = new Vec3(entity.getX() - this.getX(), entity.getY() - this.getY(), entity.getZ() - this.getZ()).scale(0.1); p_150156_.setDeltaMovement(p_150156_.getDeltaMovement().add(vec3)); } } @Override protected Entity.MovementEmission getMovementEmission() { return Entity.MovementEmission.NONE; } @Override public void remove(Entity.RemovalReason p_150146_) { this.updateOwnerInfo(null); super.remove(p_150146_); } @Override public void onClientRemoval() { this.updateOwnerInfo(null); } @Override public void setOwner(@Nullable Entity p_150154_) { super.setOwner(p_150154_); this.updateOwnerInfo(this); } private void updateOwnerInfo(@Nullable FishingHook p_150148_) { Player player = this.getPlayerOwner(); if (player != null) { player.fishing = p_150148_; } } @Nullable public Player getPlayerOwner() { return this.getOwner() instanceof Player player ? player : null; } @Nullable public Entity getHookedIn() { return this.hookedIn; } @Override public boolean canUsePortal(boolean p_344610_) { return false; } @Override public Packet getAddEntityPacket(ServerEntity p_343294_) { Entity entity = this.getOwner(); return new ClientboundAddEntityPacket(this, p_343294_, entity == null ? this.getId() : entity.getId()); } @Override public void recreateFromPacket(ClientboundAddEntityPacket p_150150_) { super.recreateFromPacket(p_150150_); if (this.getPlayerOwner() == null) { int i = p_150150_.getData(); LOGGER.error("Failed to recreate fishing hook on client. {} (id: {}) is not a valid owner.", this.level().getEntity(i), i); this.discard(); } } static enum FishHookState { FLYING, HOOKED_IN_ENTITY, BOBBING; } static enum OpenWaterType { ABOVE_WATER, INSIDE_WATER, INVALID; } }